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
3 changes: 3 additions & 0 deletions src/Turnierplan.App/Client/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { ColorThemeService } from './core/services/color-theme.service';

@Component({
selector: 'tp-root',
Expand All @@ -11,6 +12,8 @@ export class AppComponent {
private readonly translateService = inject(TranslateService);

constructor() {
inject(ColorThemeService).initialize();

this.translateService.setFallbackLang('de');
this.translateService.use('de');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { inject, Injectable } from '@angular/core';
import { LocalStorageService } from '../../portal/services/local-storage.service';
import { map, Observable, ReplaySubject } from 'rxjs';

export type ColorTheme = 'light' | 'dark';

@Injectable({
providedIn: 'root'
})
export class ColorThemeService {
private readonly localStorageService = inject(LocalStorageService);
private readonly themeSubject$ = new ReplaySubject<ColorTheme>(1);

public get theme$(): Observable<ColorTheme> {
return this.themeSubject$.asObservable();
}

public get isDarkMode$(): Observable<boolean> {
return this.themeSubject$.pipe(map((theme) => theme === 'dark'));
}

public initialize(): void {
const currentTheme = this.localStorageService.getColorTheme() === 'dark' ? 'dark' : 'light';
ColorThemeService.setColorTheme(currentTheme);
this.themeSubject$.next(currentTheme);
}

public setTheme(theme: ColorTheme): void {
this.localStorageService.setColorTheme(theme);
ColorThemeService.setColorTheme(theme);
this.themeSubject$.next(theme);
}

private static setColorTheme(theme: ColorTheme): void {
document.documentElement.setAttribute('data-bs-theme', theme);
}
}
4 changes: 4 additions & 0 deletions src/Turnierplan.App/Client/src/app/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ export const de = {
Logout: 'Abmelden'
}
},
ColorThemeSelector: {
Light: 'Hell',
Dark: 'Dunkel'
},
LandingPage: {
Title: 'Startseite',
Pages: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div class="background d-flex flex-column align-items-center justify-content-center">
<div class="shadow d-flex flex-row">
<div class="identity-image"></div>
<div class="bg-white identity-content p-4 d-flex flex-column">
<div class="bg-body identity-content p-4 d-flex flex-column">
<div class="flex-grow-1">
<router-outlet />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
@if (mode === 'IconLeftAndText') {
<button type="button" class="btn btn-sm btn-{{ type }} text-nowrap" [ngClass]="{ disabled: disabled }" (click)="buttonClick.emit()">
<button
type="button"
class="btn btn-sm btn-{{ processedType }} text-nowrap"
[ngClass]="{ disabled: disabled }"
(click)="buttonClick.emit()">
@if (icon) {
<i class="bi bi-{{ icon }}" aria-hidden="true"></i>
}
<span [translate]="title" [translateParams]="titleParams" [ngClass]="{ 'ms-2': icon }"></span>
</button>
} @else if (mode === 'IconRightAndText') {
<button type="button" class="btn btn-sm btn-{{ type }} text-nowrap" [ngClass]="{ disabled: disabled }" (click)="buttonClick.emit()">
<button
type="button"
class="btn btn-sm btn-{{ processedType }} text-nowrap"
[ngClass]="{ disabled: disabled }"
(click)="buttonClick.emit()">
<span [translate]="title" [translateParams]="titleParams" [ngClass]="{ 'me-2': icon }"></span>
@if (icon) {
<i class="bi bi-{{ icon }}" aria-hidden="true"></i>
Expand All @@ -15,7 +23,7 @@
} @else if (mode === 'IconOnly' && icon) {
<button
type="button"
class="btn btn-sm btn-{{ type }}"
class="btn btn-sm btn-{{ processedType }}"
[ngClass]="{ disabled: disabled }"
[attr.aria-label]="title | translate: titleParams"
(click)="buttonClick.emit()">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Component, EventEmitter, inject, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { NgClass } from '@angular/common';
import { TranslateDirective, TranslatePipe } from '@ngx-translate/core';
import { ColorThemeService } from '../../../core/services/color-theme.service';
import { Subject, takeUntil } from 'rxjs';

@Component({
selector: 'tp-action-button',
templateUrl: './action-button.component.html',
imports: [NgClass, TranslateDirective, TranslatePipe]
})
export class ActionButtonComponent {
export class ActionButtonComponent implements OnInit, OnDestroy {
protected processedType: string = '';

@Input()
public icon?: string;

Expand All @@ -28,4 +32,26 @@ export class ActionButtonComponent {

@Output()
public buttonClick = new EventEmitter<void>();

private readonly colorThemeService = inject(ColorThemeService);
private readonly destroyed$ = new Subject<void>();

public ngOnInit(): void {
if (this.type === 'outline-dark' || this.type === 'dark' || this.type === 'outline-light' || this.type === 'light') {
// When dark mode is enabled, certain button styles don't have sufficient contrast or are "too bright".
// These four button styles are converted to the outline / non-outline secondary version.
this.colorThemeService.isDarkMode$.pipe(takeUntil(this.destroyed$)).subscribe({
next: (isDarkMode) => {
this.processedType = isDarkMode ? this.type.replace(/dark|light/, 'secondary') : this.type;
}
});
} else {
this.processedType = this.type;
}
}

public ngOnDestroy(): void {
this.destroyed$.next();
this.destroyed$.complete();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<div class="btn-group btn-group-sm" role="group">
<input
type="radio"
class="btn-check"
name="color_theme_selector"
id="light_mode"
autocomplete="off"
[checked]="currentTheme === 'light'"
(click)="setTheme('light')" />
<label class="btn btn-{{ buttonType }}" for="light_mode">
<i class="bi bi-sun-fill"></i>
<span class="ms-2" [translate]="'Portal.ColorThemeSelector.Light'"></span>
</label>

<input
type="radio"
class="btn-check"
name="color_theme_selector"
id="dark_mode"
autocomplete="off"
[checked]="currentTheme === 'dark'"
(click)="setTheme('dark')" />
<label class="btn btn-{{ buttonType }}" for="dark_mode">
<i class="bi bi-moon-stars-fill"></i>
<span class="ms-2" [translate]="'Portal.ColorThemeSelector.Dark'"></span>
</label>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Component, inject } from '@angular/core';
import { ColorTheme, ColorThemeService } from '../../../core/services/color-theme.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Visibility } from '../../../api/models/visibility';
import { TranslateDirective } from '@ngx-translate/core';

@Component({
selector: 'tp-color-theme-selector',
imports: [TranslateDirective],
templateUrl: './color-theme-selector.component.html'
})
export class ColorThemeSelectorComponent {
protected currentTheme: ColorTheme = 'light';
protected buttonType: string = '';

private readonly colorThemeService = inject(ColorThemeService);

constructor() {
this.colorThemeService.theme$.pipe(takeUntilDestroyed()).subscribe((theme) => {
this.currentTheme = theme;
this.buttonType = theme === 'light' ? 'outline-dark' : 'outline-light';
});
}

protected readonly Visibility = Visibility;

protected setTheme(theme: ColorTheme) {
this.colorThemeService.setTheme(theme);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
@if (selectedNode) {
@for (entry of selectedNode.tournaments; track entry.id; let isLast = $last) {
<div
class="invisible-button overflow-hidden text-nowrap tp-cursor-pointer d-flex flex-row align-items-center gap-2 px-2 py-1"
class="tp-invisible-button overflow-hidden text-nowrap tp-cursor-pointer d-flex flex-row align-items-center gap-2 px-2 py-1"
[ngClass]="{ 'fw-bold': entry.id === selectedTournamentId, 'border-bottom': !isLast }"
(click)="selectTournament(entry.id)">
<i class="bi bi-trophy"></i>
Expand All @@ -34,7 +34,7 @@
} @else {
@for (document of documents; track document.id; let isLast = $last) {
<div
class="invisible-button overflow-hidden text-nowrap tp-cursor-pointer d-flex flex-row align-items-center gap-2 px-2 py-1"
class="tp-invisible-button overflow-hidden text-nowrap tp-cursor-pointer d-flex flex-row align-items-center gap-2 px-2 py-1"
[ngClass]="{ 'border-bottom': !isLast }"
(click)="selectDocument(document.id)">
<i class="bi bi-file-earmark-ruled"></i>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,3 @@
// 480px is the combined height of the loading indicator and its y margin.
min-height: 480px;
}

.invisible-button:hover {
background: #f7f7f7;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
</div>
<div class="modal-body d-flex flex-column gap-2">
<div class="mb-4" translate="Portal.ViewTournament.AddDocumentDialog.Info"></div>
<div class="d-flex flex-row flex-wrap gap-3">
@for (entry of documentTypes; track entry) {
<div class="paper tp-cursor-pointer d-flex flex-column align-items-center justify-content-center" (click)="selected(entry.type)">
<div class="earmark"></div>
<span class="fw-bold">{{ entry.displayName }}</span>
</div>
}
</div>
<table class="table table-hover table-bordered">
<tbody>
@for (entry of documentTypes; track entry) {
<tr>
<td class="tp-cursor-pointer" (click)="selected(entry.type)">
<i class="bi bi-file-earmark"></i>
<span class="ms-2">{{ entry.displayName }}</span>
</td>
</tr>
}
</tbody>
</table>
</div>
</ng-container>
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,3 @@
// 480px is the combined height of the loading indicator and its y margin.
min-height: 480px;
}

// The document preview should have the same aspect ratio as DIN pages. Therefore, we need a factor
// by which we divide the DIN A4 measurements in cm to get usable values in em.
$factor: 2.6;

.paper {
background: #fefefe;
box-shadow: 1px 1px 3px rgb(0 0 0 / 20%);
height: calc(29.7em / $factor);
position: relative;
width: calc(21em / $factor);

.earmark {
border-color: #f6f6f6 #fff;
border-style: solid;
border-width: 0 0 1.6em 1.6em;
box-shadow:
-2px -2px 0 2px rgb(255 255 255),
1px 1px 3px rgb(0 0 0 / 20%); // First shadow hides corner shadow of paper, second shadow adds earmark shadow
left: 0;
position: absolute;
top: 0;
}

&:hover {
background: #f7f7f7;

.earmark {
border-color: #f0f0f0 #fff;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
@if (compact) {
<tp-tooltip-icon [margin]="false" [tooltipText]="'Portal.IdWidget.InfoText'" />
}
<span class="font-monospace id-value">{{ id }}</span>
<span class="font-monospace tp-monospace-box">{{ id }}</span>
<tp-copy-to-clipboard [value]="id" />
</div>

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { TooltipIconComponent } from '../tooltip-icon/tooltip-icon.component';
@Component({
selector: 'tp-id-widget',
imports: [TranslateDirective, CopyToClipboardComponent, TooltipIconComponent],
templateUrl: './id-widget.component.html',
styleUrl: 'id-widget.component.scss'
templateUrl: './id-widget.component.html'
})
export class IdWidgetComponent {
@Input()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@
@for (image of existingImages; track image.id) {
<div
class="border shadow-sm image-tile position-relative"
[ngClass]="{ 'border-success current': image.id === currentImageId, 'hover-override': image.id === hoverOverrideImageId }">
[ngClass]="{
'border-success bg-success-subtle': image.id === currentImageId,
'hover-override': image.id === hoverOverrideImageId
}">
<div
class="hover-buttons d-none position-absolute flex-row align-items-center justify-content-center gap-2"
style="top: 0; left: 0">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@
}

.image-tile {
&.current {
background: repeating-linear-gradient(135deg, #e5f3e6, #e5f3e6 10px, #daf1db 10px, #daf1db 20px);
}

&:hover,
&.hover-override {
border-color: #333 !important;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

.lds-grid div {
animation: lds-grid 1.2s linear infinite;
background: #555;
background: #888;
border-radius: 50%;
height: 16px;
position: absolute;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
</div>
<div class="position-relative">
<hr class="my-2" />
<div class="position-absolute end-0 top-50 text-center bg-white" style="width: 3em; transform: translateY(calc(-50% - 2px))">
<div class="position-absolute end-0 top-50 text-center bg-body" style="width: 3em; transform: translateY(calc(-50% - 2px))">
@if (match.showLoadingIndicator) {
<tp-small-spinner />
} @else if (match.isLive) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
<tp-small-spinner />
} @else if (team.entryFeePaidAt) {
<i class="bi bi-check-circle-fill text-success"></i>
<span class="small text-secondary">
<span class="small">
<span translate="Portal.ViewTournament.Teams.EntryFeePaid.PaidAt"></span>
<span class="ms-1">{{ team.entryFeePaidAt | translateDate: 'medium' }}</span>
</span>
Expand All @@ -128,7 +128,7 @@
}
} @else {
<i class="bi bi-x-circle text-danger"></i>
<span class="small text-secondary" translate="Portal.ViewTournament.Teams.EntryFeePaid.NotPaid"></span>
<span class="small" translate="Portal.ViewTournament.Teams.EntryFeePaid.NotPaid"></span>
@if (tournamentConductAllowed) {
<tp-action-button
icon="cash-coin"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
<hr class="d-lg-none my-3" />
<div class="w-lg-50 pe-2 d-flex flex-column">
@if (selectedNode) {
@for (entry of selectedNode.tournaments; track entry.id) {
@for (entry of selectedNode.tournaments; track entry.id; let isLast = $last) {
<div
class="invisible-button overflow-hidden text-nowrap tp-cursor-pointer d-flex flex-row align-items-center gap-2 px-2 py-1"
class="tp-invisible-button overflow-hidden text-nowrap tp-cursor-pointer d-flex flex-row align-items-center gap-2 px-2 py-1"
[class]="{ 'border-bottom': !isLast }"
(click)="modal.close(entry.id)">
<i class="bi bi-trophy"></i>
<span>{{ entry.name }}</span>
Expand Down
Loading