3 Commits

Author SHA1 Message Date
8c637addf5 fix monitor bg contrast
Some checks failed
Angular Build / build (push) Has been cancelled
2026-04-27 23:01:25 +02:00
e4bbe6e75c fix mobile buttons 2026-04-27 22:34:35 +02:00
30115da841 fix styling 2026-04-27 22:02:15 +02:00
46 changed files with 832 additions and 633 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -36,7 +36,7 @@
filter: blur(10px);
&.visible {
opacity: 0.5;
opacity: 0.7;
}
}
@@ -54,7 +54,7 @@
filter: blur(5px);
&.visible {
opacity: 0.4;
opacity: 0.6;
}
}
@@ -63,7 +63,7 @@
filter: blur(8px);
&.visible {
opacity: 0.2;
opacity: 0.4;
}
}
@@ -72,7 +72,7 @@
filter: blur(8px);
&.visible {
opacity: 0.2;
opacity: 0.4;
}
}

View File

@@ -1,119 +1,128 @@
@if (show) {
<app-card [closeIcon]="faIcon" [heading]="show.showType | showType" [subheading]="show.date.toDate() | date:'dd.MM.yyyy'" closeLink="/presentation/select">
@if (!progress) {
<div class="song">
@if (show) {
<div class="song-parts">
<div
(click)="onSectionClick('title', -1, show.id)"
(keydown.enter)="onSectionClick('title', -1, show.id)"
(keydown.space)="onSectionClick('title', -1, show.id)"
[class.active]="show.presentationSongId === 'title'"
class="song-part"
role="button"
tabindex="0"
>
<div class="head">Veranstaltung</div>
</div>
<div
(click)="onSectionClick('empty', -1, show.id)"
(keydown.enter)="onSectionClick('empty', -1, show.id)"
(keydown.space)="onSectionClick('empty', -1, show.id)"
[class.active]="show.presentationSongId === 'empty'"
class="song-part"
role="button"
tabindex="0"
>
<div class="head">Leer</div>
</div>
</div>
}
</div>
@for (song of presentationSongs; track trackBy($index, song)) {
<div class="song">
@if (show) {
<div [class.active]="show.presentationSongId === song.id" class="title song-part">
<div
(click)="onSectionClick(song.id, -1, show.id)"
(keydown.enter)="onSectionClick(song.id, -1, show.id)"
(keydown.space)="onSectionClick(song.id, -1, show.id)"
class="head"
role="button"
tabindex="0"
>
{{ song.title }}
</div>
</div>
} @if (show) {
<div class="song-parts">
@for (section of song.sections; track section.type + '-' + section.number + '-' + $index; let i = $index) {
<div
(click)="onSectionClick(song.id, i, show.id)"
(keydown.enter)="onSectionClick(song.id, i, show.id)"
(keydown.space)="onSectionClick(song.id, i, show.id)"
[class.active]="
<app-page-frame title="Präsentation" [withMenu]="false">
@if (show) {
<app-card [closeIcon]="faIcon"
[heading]="show.showType | showType"
[subheading]="show.date.toDate() | date:'dd.MM.yyyy'"
closeLink="/presentation/select"
content>
@if (!progress) {
<div class="song">
@if (show) {
<div class="song-parts">
<div
(click)="onSectionClick('title', -1, show.id)"
(keydown.enter)="onSectionClick('title', -1, show.id)"
(keydown.space)="onSectionClick('title', -1, show.id)"
[class.active]="show.presentationSongId === 'title'"
class="song-part"
role="button"
tabindex="0"
>
<div class="head">Veranstaltung</div>
</div>
<div
(click)="onSectionClick('empty', -1, show.id)"
(keydown.enter)="onSectionClick('empty', -1, show.id)"
(keydown.space)="onSectionClick('empty', -1, show.id)"
[class.active]="show.presentationSongId === 'empty'"
class="song-part"
role="button"
tabindex="0"
>
<div class="head">Leer</div>
</div>
</div>
}
</div>
@for (song of presentationSongs; track trackBy($index, song)) {
<div class="song">
@if (show) {
<div [class.active]="show.presentationSongId === song.id" class="title song-part">
<div
(click)="onSectionClick(song.id, -1, show.id)"
(keydown.enter)="onSectionClick(song.id, -1, show.id)"
(keydown.space)="onSectionClick(song.id, -1, show.id)"
class="head"
role="button"
tabindex="0"
>
{{ song.title }}
</div>
</div>
}
@if (show) {
<div class="song-parts">
@for (section of song.sections; track section.type + '-' + section.number + '-' + $index; let i = $index) {
<div
(click)="onSectionClick(song.id, i, show.id)"
(keydown.enter)="onSectionClick(song.id, i, show.id)"
(keydown.space)="onSectionClick(song.id, i, show.id)"
[class.active]="
show.presentationSongId === song.id &&
show.presentationSection === i
"
class="song-part"
role="button"
tabindex="0"
>
<div class="head">{{ section.type | sectionType }} {{ section.number + 1 }}</div>
<div class="fragment">{{ getFirstLine(section) }}</div>
</div>
class="song-part"
role="button"
tabindex="0"
>
<div class="head">{{ section.type | sectionType }} {{ section.number + 1 }}</div>
<div class="fragment">{{ getFirstLine(section) }}</div>
</div>
}
</div>
}
</div>
}
<div class="song">
@if (show) {
<div [class.active]="show.presentationSongId === 'dynamicText'" class="title song-part">
<div
(click)="onSectionClick('dynamicText', -1, show.id)"
(keydown.enter)="onSectionClick('dynamicText', -1, show.id)"
(keydown.space)="onSectionClick('dynamicText', -1, show.id)"
class="head"
role="button"
tabindex="0"
>
Freier Text
</div>
</div>
}
<mat-form-field appearance="outline">
<mat-label>Überschrift</mat-label>
<input (ngModelChange)="onDynamicCaption($event, show.id)" [ngModel]="show.presentationDynamicCaption"
autocomplete="off" id="dynamic-caption" matInput type="text" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Text</mat-label>
<textarea (ngModelChange)="onDynamicText($event, show.id)" [ngModel]="show.presentationDynamicText"
autocomplete="off" id="dynamic-text" matInput></textarea>
</mat-form-field>
</div>
@if (show) {
<div class="div-bottom">
<app-button [icon]="faDesktop" routerLink="/presentation/monitor">Präsentation starten</app-button>
<mat-form-field appearance="outline">
<mat-label>Hintergrund</mat-label>
<mat-select (ngModelChange)="onBackground($event, show.id)" [ngModel]="show.presentationBackground">
<mat-option value="none">kein Hintergrund</mat-option>
<mat-option value="blue">Sternenhimmel</mat-option>
<mat-option value="green">Blätter</mat-option>
<mat-option value="leder">Leder</mat-option>
<mat-option value="praise">Lobpreis</mat-option>
<mat-option value="bible">Bibel</mat-option>
</mat-select>
</mat-form-field>
<mat-slider #slider [max]="100" [min]="10" [step]="2" class="zoom-slider" color="primary" ngDefaultControl
><input (ngModelChange)="onZoom($event, show.id)" [ngModel]="show.presentationZoom" matSliderThumb />
</mat-slider>
</div>
}
@if (show) {
<app-add-song [addedLive]="true" [showSongs]="showSongs" [show]="show" [songs]="songs$|async"></app-add-song>
}
}
</div>
}
</div>
</app-card>
}
<div class="song">
@if (show) {
<div [class.active]="show.presentationSongId === 'dynamicText'" class="title song-part">
<div
(click)="onSectionClick('dynamicText', -1, show.id)"
(keydown.enter)="onSectionClick('dynamicText', -1, show.id)"
(keydown.space)="onSectionClick('dynamicText', -1, show.id)"
class="head"
role="button"
tabindex="0"
>
Freier Text
</div>
</div>
}
<mat-form-field appearance="outline">
<mat-label>Überschrift</mat-label>
<input (ngModelChange)="onDynamicCaption($event, show.id)" [ngModel]="show.presentationDynamicCaption" autocomplete="off" id="dynamic-caption" matInput type="text" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Text</mat-label>
<textarea (ngModelChange)="onDynamicText($event, show.id)" [ngModel]="show.presentationDynamicText" autocomplete="off" id="dynamic-text" matInput></textarea>
</mat-form-field>
</div>
@if (show) {
<div class="div-bottom">
<button class="btn-start-presentation" mat-button routerLink="/presentation/monitor">
<fa-icon [icon]="faDesktop"></fa-icon>
Präsentation starten
</button>
<mat-form-field appearance="outline">
<mat-label>Hintergrund</mat-label>
<mat-select (ngModelChange)="onBackground($event, show.id)" [ngModel]="show.presentationBackground">
<mat-option value="none">kein Hintergrund</mat-option>
<mat-option value="blue">Sternenhimmel</mat-option>
<mat-option value="green">Blätter</mat-option>
<mat-option value="leder">Leder</mat-option>
<mat-option value="praise">Lobpreis</mat-option>
<mat-option value="bible">Bibel</mat-option>
</mat-select>
</mat-form-field>
<mat-slider #slider [max]="100" [min]="10" [step]="2" class="zoom-slider" color="primary" ngDefaultControl
><input (ngModelChange)="onZoom($event, show.id)" [ngModel]="show.presentationZoom" matSliderThumb />
</mat-slider>
</div>
} @if (show) {
<app-add-song [addedLive]="true" [showSongs]="showSongs" [show]="show" [songs]="songs$|async"></app-add-song>
} }
</app-card>
}
</app-page-frame>

View File

@@ -1,4 +1,4 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, inject} from '@angular/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnDestroy} from '@angular/core';
import {combineLatest, Subject} from 'rxjs';
import {PresentationBackground, Show} from '../../shows/services/show';
import {ShowSongService} from '../../shows/services/show-song.service';
@@ -17,15 +17,15 @@ import {CardComponent} from '../../../widget-modules/components/card/card.compon
import {MatFormField, MatLabel} from '@angular/material/form-field';
import {MatInput} from '@angular/material/input';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatButton} from '@angular/material/button';
import {RouterLink} from '@angular/router';
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {MatSelect} from '@angular/material/select';
import {MatOption} from '@angular/material/core';
import {MatSlider, MatSliderThumb} from '@angular/material/slider';
import {AddSongComponent} from '../../../widget-modules/components/add-song/add-song.component';
import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/show-type.pipe';
import {SectionTypePipe} from '../../../widget-modules/pipes/section-type-translator/section-type.pipe';
import {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
export interface PresentationSong {
id: string;
@@ -46,9 +46,7 @@ export interface PresentationSong {
MatInput,
ReactiveFormsModule,
FormsModule,
MatButton,
RouterLink,
FaIconComponent,
MatSelect,
MatOption,
MatSlider,
@@ -58,6 +56,8 @@ export interface PresentationSong {
DatePipe,
ShowTypePipe,
SectionTypePipe,
PageFrameComponent,
ButtonComponent,
],
})
export class RemoteComponent implements OnDestroy {
@@ -87,12 +87,12 @@ export class RemoteComponent implements OnDestroy {
map(_ => _.currentShow),
filter((showId): showId is string => !!showId),
distinctUntilChanged(),
takeUntil(this.destroy$)
takeUntil(this.destroy$),
);
const show$ = currentShowId$.pipe(
switchMap(showId => this.showService.read$(showId)),
takeUntil(this.destroy$)
takeUntil(this.destroy$),
);
const parsedSongs$ = currentShowId$.pipe(
@@ -105,7 +105,7 @@ export class RemoteComponent implements OnDestroy {
sections: this.textRenderingService.parse(song.text, null, false),
})),
})),
takeUntil(this.destroy$)
takeUntil(this.destroy$),
);
combineLatest([show$, parsedSongs$])

View File

@@ -1,20 +1,24 @@
@if (shows$ | async; as shows) {
<div @fade>
@if (visible) {
<app-card heading="Bitte eine Veranstaltung auswählen">
@if (!shows.length) {
<p>Es ist derzeit keine Veranstaltung vorhanden</p>
} @if (shows.length>0) {
<div class="list">
@for (show of shows; track show.id) {
<button (click)="selectShow(show)" mat-stroked-button>
<app-user-name [userId]="show.owner"></app-user-name>
, {{ show.showType | showType }}, {{ show.date.toDate() | date: "dd.MM.yyyy" }}
</button>
<app-page-frame title="Präsentation" [withMenu]="false">
@if (shows$ | async; as shows) {
<div @fade content>
@if (visible) {
<app-card heading="Bitte eine Veranstaltung auswählen">
@if (!shows.length) {
<p>Es ist derzeit keine Veranstaltung vorhanden</p>
}
@if (shows.length > 0) {
<div class="list">
@for (show of shows; track show.id) {
<app-button (click)="selectShow(show)" class="full-width">
<app-user-name [userId]="show.owner"></app-user-name>
, {{ show.showType | showType }}, {{ show.date.toDate() | date: "dd.MM.yyyy" }}
</app-button>
}
</div>
}
</app-card>
}
</div>
}
</app-card>
}
</div>
}
</app-page-frame>

View File

@@ -1,4 +1,4 @@
import {Component, OnInit, inject} from '@angular/core';
import {Component, inject, OnInit} from '@angular/core';
import {map} from 'rxjs/operators';
import {ShowService} from '../../shows/services/show.service';
import {Show} from '../../shows/services/show';
@@ -7,16 +7,17 @@ import {Router} from '@angular/router';
import {fade} from '../../../animations';
import {AsyncPipe, DatePipe} from '@angular/common';
import {CardComponent} from '../../../widget-modules/components/card/card.component';
import {MatButton} from '@angular/material/button';
import {UserNameComponent} from '../../../services/user/user-name/user-name.component';
import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/show-type.pipe';
import {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
@Component({
selector: 'app-select',
templateUrl: './select.component.html',
styleUrls: ['./select.component.less'],
animations: [fade],
imports: [CardComponent, MatButton, UserNameComponent, AsyncPipe, DatePipe, ShowTypePipe],
imports: [CardComponent, UserNameComponent, AsyncPipe, DatePipe, ShowTypePipe, PageFrameComponent, ButtonComponent],
})
export class SelectComponent implements OnInit {
private showService = inject(ShowService);

View File

@@ -1,5 +1,6 @@
<div mat-dialog-content>
<p>Bitte melde die in dieser Veranstaltung verwendeten CCLI-Titel. Die Meldung ist Teil der CCLI-Lizenz und sorgt dafür, dass Songwriter und Verlage korrekt vergütet werden.</p>
<p>Bitte melde die in dieser Veranstaltung verwendeten CCLI-Titel. Die Meldung ist Teil der CCLI-Lizenz und sorgt
dafür, dass Songwriter und Verlage korrekt vergütet werden.</p>
<p>
Die Meldung erfolgt über
<a [href]="reportingUrl" rel="noreferrer" target="_blank">{{ reportingUrl }}</a>.
@@ -12,29 +13,29 @@
</div>
@for (song of data.songs; track song.title + song.ccliNumber) {
<div class="list-item">
<div>{{ song.title }}</div>
<div class="number-cell">
<span>{{ song.ccliNumber }}</span>
<a
(click)="markOpened(song.ccliNumber)"
[attr.aria-label]="'CCLI-Titel melden: ' + song.title"
[href]="getSongReportingUrl(song.ccliNumber)"
rel="noreferrer"
target="_blank"
class="btn-icon report-link"
>
<fa-icon [icon]="faOpen"></fa-icon>
</a>
@if (wasOpened(song.ccliNumber)) {
<fa-icon [icon]="faCheck" class="opened-check"></fa-icon>
}
<div class="list-item">
<div>{{ song.title }}</div>
<div class="number-cell">
<span>{{ song.ccliNumber }}</span>
<a
(click)="markOpened(song.ccliNumber)"
[attr.aria-label]="'CCLI-Titel melden: ' + song.title"
[href]="getSongReportingUrl(song.ccliNumber)"
rel="noreferrer"
target="_blank"
class="btn-icon report-link"
>
<fa-icon [icon]="faOpen"></fa-icon>
</a>
@if (wasOpened(song.ccliNumber)) {
<fa-icon [icon]="faCheck" class="opened-check"></fa-icon>
}
</div>
</div>
</div>
}
</div>
</div>
<div mat-dialog-actions>
<button [mat-dialog-close]="false" mat-button>Abbrechen</button>
<button [mat-dialog-close]="true" cdkFocusInitial mat-button>Alle CCLI-Titel wurden gemeldet</button>
</div>
<app-button-row>
<app-button (click)="dialogRef.close(false)" [icon]="faClose">Abbrechen</app-button>
<app-button (click)="dialogRef.close(true)" [icon]="faReport">Alle CCLI-Titel wurden gemeldet</app-button>
</app-button-row>

View File

@@ -72,3 +72,7 @@
justify-content: flex-start;
}
}
app-button-row {
margin: 0 var(--gap-l) var(--gap-l);
}

View File

@@ -1,8 +1,9 @@
import {Component, inject} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogActions, MatDialogClose, MatDialogContent} from '@angular/material/dialog';
import {MatButton} from '@angular/material/button';
import {MAT_DIALOG_DATA, MatDialogContent, MatDialogRef} from '@angular/material/dialog';
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {faArrowUpRightFromSquare, faCheck} from '@fortawesome/free-solid-svg-icons';
import {faArrowUpRightFromSquare, faCheck, faCompactDisc, faXmark} from '@fortawesome/free-solid-svg-icons';
import {ButtonRowComponent} from '../../../../widget-modules/components/button-row/button-row.component';
import {ButtonComponent} from '../../../../widget-modules/components/button/button.component';
export interface ReportDialogSong {
title: string;
@@ -15,7 +16,7 @@ export interface ReportDialogData {
@Component({
selector: 'app-report-dialog',
imports: [MatButton, MatDialogActions, MatDialogContent, MatDialogClose, FaIconComponent],
imports: [MatDialogContent, FaIconComponent, ButtonRowComponent, ButtonComponent],
templateUrl: './report-dialog.component.html',
styleUrl: './report-dialog.component.less',
standalone: true,
@@ -25,6 +26,11 @@ export class ReportDialogComponent {
public readonly faOpen = faArrowUpRightFromSquare;
public readonly faCheck = faCheck;
public data = inject<ReportDialogData>(MAT_DIALOG_DATA);
public dialogRef = inject(MatDialogRef<ReportDialogComponent>);
public faReport = faCompactDisc;
public faClose = faXmark;
private readonly openedNumbers = new Set<string>();
public getSongReportingUrl(ccliNumber: string): string {

View File

@@ -1,31 +1,33 @@
<div>
<app-card [closeLink]="'/shows/'+form.value.id" heading="Veranstaltung ändern">
<div [formGroup]="form" class="split">
<mat-form-field appearance="outline">
<mat-label>Art der Veranstaltung</mat-label>
<mat-select formControlName="showType">
<mat-optgroup label="öffentlich">
@for (key of showTypePublic; track key) {
<mat-option [value]="key">{{ key | showType }} </mat-option>
}
</mat-optgroup>
<mat-optgroup label="privat">
@for (key of showTypePrivate; track key) {
<mat-option [value]="key">{{ key | showType }} </mat-option>
}
</mat-optgroup>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Datum</mat-label>
<input [matDatepicker]="picker" formControlName="date" matInput />
<mat-datepicker-toggle [for]="picker" matSuffix></mat-datepicker-toggle>
<mat-datepicker #picker touchUi></mat-datepicker>
</mat-form-field>
</div>
<app-page-frame title="Veranstaltungen" [withMenu]="false">
<div content>
<app-card [closeLink]="'/shows/'+form.value.id" heading="Veranstaltung ändern">
<div [formGroup]="form" class="split">
<mat-form-field appearance="outline">
<mat-label>Art der Veranstaltung</mat-label>
<mat-select formControlName="showType">
<mat-optgroup label="öffentlich">
@for (key of showTypePublic; track key) {
<mat-option [value]="key">{{ key | showType }}</mat-option>
}
</mat-optgroup>
<mat-optgroup label="privat">
@for (key of showTypePrivate; track key) {
<mat-option [value]="key">{{ key | showType }}</mat-option>
}
</mat-optgroup>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Datum</mat-label>
<input [matDatepicker]="picker" formControlName="date" matInput />
<mat-datepicker-toggle [for]="picker" matSuffix></mat-datepicker-toggle>
<mat-datepicker #picker touchUi></mat-datepicker>
</mat-form-field>
</div>
<app-button-row>
<app-button (click)="onSave()" [icon]="faSave">Speichern</app-button>
</app-button-row>
</app-card>
</div>
<app-button-row>
<app-button (click)="onSave()" [icon]="faSave">Speichern</app-button>
</app-button-row>
</app-card>
</div>
</app-page-frame>

View File

@@ -1,4 +1,4 @@
import {Component, OnInit, inject} from '@angular/core';
import {Component, inject, OnInit} from '@angular/core';
import {ShowDataService} from '../services/show-data.service';
import {Observable, take} from 'rxjs';
import {Show} from '../services/show';
@@ -18,6 +18,7 @@ import {MatDatepicker, MatDatepickerInput, MatDatepickerToggle} from '@angular/m
import {ButtonRowComponent} from '../../../widget-modules/components/button-row/button-row.component';
import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/show-type.pipe';
import {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
@Component({
selector: 'app-edit',
@@ -39,6 +40,7 @@ import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/s
ButtonRowComponent,
ButtonComponent,
ShowTypePipe,
PageFrameComponent,
],
})
export class EditComponent implements OnInit {
@@ -70,7 +72,7 @@ export class EditComponent implements OnInit {
map(param => param as {showId: string}),
map(param => param.showId),
switchMap((showId: string) => this.showService.read$(showId)),
take(1)
take(1),
)
.subscribe(show => {
this.form.setValue({

View File

@@ -1,14 +1,14 @@
@if (show) {
<div class="list-item">
<div>{{ show.date.toDate() | date: "dd.MM.yyyy" }}</div>
<div>
<app-user-name [userId]="show.owner"></app-user-name>
<div class="list-item">
<div class="date">{{ show.date.toDate() | date: "dd.MM.yyyy" }}</div>
<div class="user">
<app-user-name [userId]="show.owner"></app-user-name>
</div>
<div class="show-type">{{ show.showType | showType }}</div>
<div class="badge">
@if (showStatusBadge) {
<app-badge [type]="showStatusBadgeType">{{ showStatusBadge }}</app-badge>
}
</div>
</div>
<div>{{ show.showType | showType }}</div>
<div>
@if (showStatusBadge) {
<app-badge [type]="showStatusBadgeType">{{ showStatusBadge }}</app-badge>
}
</div>
</div>
}

View File

@@ -2,6 +2,15 @@
padding: 5px 20px;
display: grid;
grid-template-columns: 100px 150px auto 160px;
grid-template-areas: "date user show-type badge";
@media screen and (max-width: 860px) {
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: auto auto;
grid-template-areas: "date user badge" ". show-type badge";
row-gap: 3px;
}
min-height: 21px;
& > div {
@@ -16,6 +25,23 @@
background: var(--hover-background);
color: var(--text);
}
.date {
grid-area: date;
}
.user {
grid-area: user;
}
.show-type {
grid-area: show-type;
}
.badge {
grid-area: badge;
justify-content: end;
}
}
.number {

View File

@@ -1,40 +1,45 @@
@if (showSidebar$ | async) {
<app-sidebar>
<div class="sidebar-content" sidebar>
<app-filter [shows]="(publicShows$ | async) ?? []"></app-filter>
</div>
<div content>
@if (privateShows$ | async; as privateShows) {
<app-card [padding]="false" heading="Meine Veranstaltungen">
@for (show of privateShows; track trackBy($index, show)) {
<app-list-item
[routerLink]="show.id"
[showStatusBadgeType]="show.archived ? 'warn' : show.published ? 'error' : 'none'"
[showStatusBadge]="show.archived ? 'archiviert' : show.published ? 'nicht gemeldet' : 'unveröffentlicht'"
[show]="show"
></app-list-item>
<app-page-frame title="Veranstaltungen">
<div class="sidebar-content" sidebar>
<app-filter [shows]="(publicShows$ | async) ?? []"></app-filter>
</div>
<div content>
@if (privateShows$ | async; as privateShows) {
<app-card [padding]="false" heading="Meine Veranstaltungen">
@for (show of privateShows; track trackBy($index, show)) {
<app-list-item
[routerLink]="show.id"
[showStatusBadgeType]="show.archived ? 'warn' : show.published ? 'error' : 'none'"
[showStatusBadge]="show.archived ? 'archiviert' : show.published ? 'nicht gemeldet' : 'unveröffentlicht'"
[show]="show"
></app-list-item>
}
<div *appRole="['leader']" class="list-action">
<app-button [fullWidth]="true" [icon]="faNewShow" routerLink="new">Neue Veranstaltung anlegen</app-button>
</div>
</app-card>
}
<div *appRole="['leader']" class="list-action">
<app-button [fullWidth]="true" [icon]="faNewShow" routerLink="new">Neue Veranstaltung anlegen </app-button>
</div>
</app-card>
} @if (publicShows$ | async; as shows) { @if (shows.length > 0) {
<app-card [padding]="false" heading="Veröffentlichte Veranstaltungen">
@for (show of shows; track trackBy($index, show)) {
<app-list-item [routerLink]="show.id" [show]="show"></app-list-item>
@if (publicShows$ | async; as shows) {
@if (shows.length > 0) {
<app-card [padding]="false" heading="Veröffentlichte Veranstaltungen">
@for (show of shows; track trackBy($index, show)) {
<app-list-item [routerLink]="show.id" [show]="show"></app-list-item>
}
</app-card>
}
}
</app-card>
} }
</div>
</app-sidebar>
</div>
</app-page-frame>
} @else {
<div>
@if (publicShows$ | async; as shows) { @if (shows.length > 0) {
<app-card [padding]="false" heading="Veröffentlichte Veranstaltungen">
@for (show of shows; track trackBy($index, show)) {
<app-list-item [routerLink]="show.id" [show]="show"></app-list-item>
<div>
@if (publicShows$ | async; as shows) {
@if (shows.length > 0) {
<app-card [padding]="false" heading="Veröffentlichte Veranstaltungen">
@for (show of shows; track trackBy($index, show)) {
<app-list-item [routerLink]="show.id" [show]="show"></app-list-item>
}
</app-card>
}
}
</app-card>
} }
</div>
</div>
}

View File

@@ -1,3 +1,7 @@
.sidebar-content {
padding: 20px;
}
.list-action {
margin: 10px 20px;
}

View File

@@ -12,7 +12,7 @@ import {FilterComponent} from './filter/filter.component';
import {CardComponent} from '../../../widget-modules/components/card/card.component';
import {ListItemComponent} from './list-item/list-item.component';
import {UserService} from '../../../services/user/user.service';
import {SidebarComponent} from '../../../widget-modules/components/sidebar/sidebar.component';
import {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
import {faPlus} from '@fortawesome/free-solid-svg-icons';
import {RoleDirective} from '../../../services/user/role.directive';
@@ -22,21 +22,33 @@ import {RoleDirective} from '../../../services/user/role.directive';
templateUrl: './list.component.html',
styleUrls: ['./list.component.less'],
animations: [fade],
imports: [FilterComponent, CardComponent, ListItemComponent, RouterLink, AsyncPipe, SidebarComponent, ButtonComponent, RoleDirective],
imports: [FilterComponent, CardComponent, ListItemComponent, RouterLink, AsyncPipe, PageFrameComponent, ButtonComponent, RoleDirective],
})
export class ListComponent {
public faNewShow = faPlus;
private showService = inject(ShowService);
private filterStore = inject(FilterStoreService);
private userService = inject(UserService);
public filter$ = this.filterStore.showFilter$;
public lastMonths$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.time || 1));
public owner$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.owner));
public showType$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.showType));
public archived$ = this.filter$.pipe(map((filterValues: FilterValues) => !!filterValues.archived));
private showService = inject(ShowService);
public shows$ = this.showService.list$();
public fallbackPublicShows$ = combineLatest([this.shows$, this.lastMonths$]).pipe(
map(([shows, lastMonths]) => {
return shows.filter(show => show.published && !show.archived).filter(show => this.matchesTimeFilter(show, lastMonths));
}),
);
public ownShows$ = this.showService.list$(false, true);
public queriedPublicShows$ = this.lastMonths$.pipe(switchMap(lastMonths => this.showService.listPublicSince$(lastMonths)));
public publicShows$ = combineLatest([this.queriedPublicShows$, this.fallbackPublicShows$, this.owner$, this.showType$]).pipe(
map(([queriedShows, fallbackShows, owner, showType]) => {
const shows = queriedShows.length > 0 || fallbackShows.length === 0 ? queriedShows : fallbackShows;
return this.sortShowsByDateDesc(shows.filter(show => !owner || show.owner === owner).filter(show => !showType || show.showType === showType));
}),
);
private userService = inject(UserService);
public privateShows$ = combineLatest([this.ownShows$, this.userService.user$, this.archived$]).pipe(
map(([shows, user, showArchived]) =>
shows.filter(show => {
@@ -49,22 +61,9 @@ export class ListComponent {
}
return !show.published || show.reportedType === 'pending';
})
}),
),
map(shows => this.sortShowsByDateDesc(shows))
);
public queriedPublicShows$ = this.lastMonths$.pipe(switchMap(lastMonths => this.showService.listPublicSince$(lastMonths)));
public fallbackPublicShows$ = combineLatest([this.shows$, this.lastMonths$]).pipe(
map(([shows, lastMonths]) => {
return shows.filter(show => show.published && !show.archived).filter(show => this.matchesTimeFilter(show, lastMonths));
})
);
public publicShows$ = combineLatest([this.queriedPublicShows$, this.fallbackPublicShows$, this.owner$, this.showType$]).pipe(
map(([queriedShows, fallbackShows, owner, showType]) => {
const shows = queriedShows.length > 0 || fallbackShows.length === 0 ? queriedShows : fallbackShows;
return this.sortShowsByDateDesc(shows.filter(show => !owner || show.owner === owner).filter(show => !showType || show.showType === showType));
})
map(shows => this.sortShowsByDateDesc(shows)),
);
public showSidebar$ = this.userService.user$.pipe(map(user => this.hasSidebarAccess(user?.role)));

View File

@@ -1,31 +1,33 @@
<div>
<app-card closeLink="/shows" heading="Neue Veranstaltung">
<div [formGroup]="form" class="split">
<mat-form-field appearance="outline">
<mat-label>Art der Veranstaltung</mat-label>
<mat-select formControlName="showType">
<mat-optgroup label="öffentlich">
@for (key of showTypePublic; track key) {
<mat-option [value]="key">{{ key | showType }} </mat-option>
}
</mat-optgroup>
<mat-optgroup label="privat">
@for (key of showTypePrivate; track key) {
<mat-option [value]="key">{{ key | showType }} </mat-option>
}
</mat-optgroup>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Datum</mat-label>
<input [matDatepicker]="picker" formControlName="date" matInput />
<mat-datepicker-toggle [for]="picker" matSuffix></mat-datepicker-toggle>
<mat-datepicker #picker touchUi></mat-datepicker>
</mat-form-field>
</div>
<app-page-frame title="Veranstaltungen" [withMenu]="false">
<div content>
<app-card closeLink="/shows" heading="Neue Veranstaltung">
<div [formGroup]="form" class="split">
<mat-form-field appearance="outline">
<mat-label>Art der Veranstaltung</mat-label>
<mat-select formControlName="showType">
<mat-optgroup label="öffentlich">
@for (key of showTypePublic; track key) {
<mat-option [value]="key">{{ key | showType }}</mat-option>
}
</mat-optgroup>
<mat-optgroup label="privat">
@for (key of showTypePrivate; track key) {
<mat-option [value]="key">{{ key | showType }}</mat-option>
}
</mat-optgroup>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Datum</mat-label>
<input [matDatepicker]="picker" formControlName="date" matInput />
<mat-datepicker-toggle [for]="picker" matSuffix></mat-datepicker-toggle>
<mat-datepicker #picker touchUi></mat-datepicker>
</mat-form-field>
</div>
<app-button-row>
<app-button (click)="onSave()" [icon]="faSave">Anlegen</app-button>
</app-button-row>
</app-card>
</div>
<app-button-row>
<app-button (click)="onSave()" [icon]="faSave">Anlegen</app-button>
</app-button-row>
</app-card>
</div>
</app-page-frame>

View File

@@ -1,4 +1,4 @@
import {Component, OnInit, inject} from '@angular/core';
import {Component, inject, OnInit} from '@angular/core';
import {ShowDataService} from '../services/show-data.service';
import {Observable} from 'rxjs';
import {Show} from '../services/show';
@@ -16,6 +16,7 @@ import {MatDatepicker, MatDatepickerInput, MatDatepickerToggle} from '@angular/m
import {ButtonRowComponent} from '../../../widget-modules/components/button-row/button-row.component';
import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/show-type.pipe';
import {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
@Component({
selector: 'app-new',
@@ -37,6 +38,7 @@ import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/s
ButtonRowComponent,
ButtonComponent,
ShowTypePipe,
PageFrameComponent,
],
})
export class NewComponent implements OnInit {

View File

@@ -1,110 +1,127 @@
@if (show$ | async; as show) {
<div>
<app-card
[fullscreen]="useSwiper"
closeLink="../"
heading="{{ show.showType | showType }}, {{
<app-page-frame title="Veranstaltungen" [withMenu]="false">
@if (show$ | async; as show) {
<div content>
<app-card
[fullscreen]="useSwiper"
closeLink="../"
heading="{{ show.showType | showType }}, {{
show.date.toDate() | date: 'dd.MM.yyyy'
}} - {{ getStatus(show) }}"
>
@if (!useSwiper) {
<p class="show-meta">
{{ show.public ? 'öffentliche' : 'geschlossene' }} Veranstaltung von
<app-user-name [userId]="show.owner"></app-user-name>
<ng-container *appOwner="show.owner">
<app-badge [type]="getPublishedBadgeType(show)">{{ show.published | publishedType }}</app-badge>
@if (show.reportedType) {
<app-badge [type]="getReportedTypeBadgeType(show)">{{ show.reportedType | reportedType }}</app-badge>
}
</ng-container>
</p>
}
<div class="head">
<div>
>
@if (!useSwiper) {
<mat-checkbox [(ngModel)]="showText">Text anzeigen</mat-checkbox>
<p class="show-meta">
{{ show.public ? 'öffentliche' : 'geschlossene' }} Veranstaltung von
<app-user-name [userId]="show.owner"></app-user-name>
<ng-container *appOwner="show.owner">
<app-badge [type]="getPublishedBadgeType(show)">{{ show.published | publishedType }}</app-badge>
@if (show.reportedType) {
<app-badge [type]="getReportedTypeBadgeType(show)">{{ show.reportedType | reportedType }}</app-badge>
}
</ng-container>
</p>
}
</div>
<div [class.floating]="useSwiper">
<app-menu-button (click)="onZoomOut()" @fade [icon]="faZoomOut" class="btn-delete btn-icon" matTooltip="Verkleinern"></app-menu-button>
<app-menu-button (click)="onZoomIn()" @fade [icon]="faZoomIn" class="btn-delete btn-icon" matTooltip="Vergrößern"></app-menu-button>
<app-menu-button
(click)="useSwiper=!useSwiper;fullscreen(useSwiper)"
@fade
[icon]="useSwiper ? faRestore : faMaximize"
class="btn-delete btn-icon"
matTooltip="Vollbild"
></app-menu-button>
</div>
</div>
@if (showSongs && !useSwiper) {
<div
(cdkDropListDropped)="drop($event, show)"
[cdkDropListDisabled]="show.published || showText"
[style.--song-key-column-width]="getSongKeyColumnWidth(show)"
[style.font-size]="textSize + 'em'"
cdkDropList
class="song-list"
>
@for (song of orderedShowSongs(show); track trackBy(i, song); let i = $index) {
<div cdkDrag class="song-row">
<app-song
[dragHandle]="!(show.published || showText)"
[fullscreen]="useSwiper"
[index]="i"
[showId]="showId"
[showSong]="song"
[showText]="showText"
[show]="show"
></app-song>
</div>
}
</div>
} @if (useSwiper) {
<swiper-container scrollbar="true">
@for (song of orderedShowSongs(show); track trackBy(i, song); let i = $index) {
<swiper-slide [style.font-size]="textSize + 'em'" class="song-swipe">
<app-song [fullscreen]="true" [index]="i" [showId]="showId" [showSong]="song" [showText]="true" [show]="show"></app-song>
<div class="time">{{ currentTime | date: 'HH:mm' }}</div>
@if (getNextSong(orderedShowSongs(show), i); as next) {
<div class="next-song">
{{ next }}
<fa-icon [icon]="faNextSong"></fa-icon>
<div class="head">
<div>
@if (!useSwiper) {
<mat-checkbox [(ngModel)]="showText">Text anzeigen</mat-checkbox>
}
</div>
<div [class.floating]="useSwiper">
<app-menu-button (click)="onZoomOut()" @fade [icon]="faZoomOut" class="btn-delete btn-icon"
matTooltip="Verkleinern"></app-menu-button>
<app-menu-button (click)="onZoomIn()" @fade [icon]="faZoomIn" class="btn-delete btn-icon"
matTooltip="Vergrößern"></app-menu-button>
<app-menu-button
(click)="useSwiper=!useSwiper;fullscreen(useSwiper)"
@fade
[icon]="useSwiper ? faRestore : faMaximize"
class="btn-delete btn-icon"
matTooltip="Vollbild"
></app-menu-button>
</div>
</div>
@if (showSongs && !useSwiper) {
<div
(cdkDropListDropped)="drop($event, show)"
[cdkDropListDisabled]="show.published || showText"
[style.--song-key-column-width]="getSongKeyColumnWidth(show)"
[style.font-size]="textSize + 'em'"
cdkDropList
class="song-list"
>
@for (song of orderedShowSongs(show); track trackBy(i, song); let i = $index) {
<div cdkDrag class="song-row">
<app-song
[dragHandle]="!(show.published || showText)"
[fullscreen]="useSwiper"
[index]="i"
[showId]="showId"
[showSong]="song"
[showText]="showText"
[show]="show"
></app-song>
</div>
}
</div>
}
</swiper-slide>
}
</swiper-container>
} @if (songs$ | async; as songs) { @if (songs && !show.published && !useSwiper) {
<app-add-song [showSongs]="showSongs" [show]="show" [songs]="songs"></app-add-song>
} } @if (!useSwiper) {
<app-button-row>
<ng-container *appRole="['leader']">
<ng-container *appOwner="show.owner">
@if (!show.archived) {
<app-button (click)="onArchive(true)" [icon]="faBox"> Archivieren</app-button>
} @if (show.archived) {
<app-button (click)="onArchive(false)" [icon]="faBoxOpen"> Wiederherstellen</app-button>
} @if (!show.published) {
<app-button (click)="onPublish(show, true)" [icon]="faPublish"> Veröffentlichen</app-button>
} @if (show.published) {
<app-button (click)="onPublish(show, false)" [icon]="faUnpublish"> Veröffentlichung zurückziehen </app-button>
} @if (show.published) {
<app-button (click)="onShare(show)" [icon]="faShare"> Teilen</app-button>
} @if (show.published && show.reportedType === 'pending') {
<app-button (click)="onReport(show)" [icon]="faReport"> CCLI</app-button>
} @if (!show.published) {
<app-button (click)="onChange(show.id)" [icon]="faSliders"> Ändern</app-button>
@if (useSwiper) {
<swiper-container scrollbar="true">
@for (song of orderedShowSongs(show); track trackBy(i, song); let i = $index) {
<swiper-slide [style.font-size]="textSize + 'em'" class="song-swipe">
<app-song [fullscreen]="true" [index]="i" [showId]="showId" [showSong]="song" [showText]="true"
[show]="show"></app-song>
<div class="time">{{ currentTime | date: 'HH:mm' }}</div>
@if (getNextSong(orderedShowSongs(show), i); as next) {
<div class="next-song">
{{ next }}
<fa-icon [icon]="faNextSong"></fa-icon>
</div>
}
</swiper-slide>
}
</swiper-container>
}
@if (songs$ | async; as songs) {
@if (songs && !show.published && !useSwiper) {
<app-add-song [showSongs]="showSongs" [show]="show" [songs]="songs"></app-add-song>
}
</ng-container>
</ng-container>
<app-button [icon]="faDownload" [matMenuTriggerFor]="menu"> Herunterladen</app-button>
<mat-menu #menu="matMenu">
<app-button (click)="onDownload()" [icon]="faUser"> Ablauf für Lobpreisgruppe</app-button>
<app-button (click)="onDownloadHandout()" [icon]="faUsers"> Handout mit Copyright Infos</app-button>
</mat-menu>
</app-button-row>
}
</app-card>
</div>
}
}
@if (!useSwiper) {
<app-button-row>
<ng-container *appRole="['leader']">
<ng-container *appOwner="show.owner">
@if (!show.archived) {
<app-button (click)="onArchive(true)" [icon]="faBox"> Archivieren</app-button>
}
@if (show.archived) {
<app-button (click)="onArchive(false)" [icon]="faBoxOpen"> Wiederherstellen</app-button>
}
@if (!show.published) {
<app-button (click)="onPublish(show, true)" [icon]="faPublish"> Veröffentlichen</app-button>
}
@if (show.published) {
<app-button (click)="onPublish(show, false)" [icon]="faUnpublish"> Veröffentlichung zurückziehen
</app-button>
}
@if (show.published) {
<app-button (click)="onShare(show)" [icon]="faShare"> Teilen</app-button>
}
@if (show.published && show.reportedType === 'pending') {
<app-button (click)="onReport(show)" [icon]="faReport"> CCLI</app-button>
}
@if (!show.published) {
<app-button (click)="onChange(show.id)" [icon]="faSliders"> Ändern</app-button>
}
</ng-container>
</ng-container>
<app-button [icon]="faDownload" [matMenuTriggerFor]="menu"> Herunterladen</app-button>
<mat-menu #menu="matMenu">
<app-button (click)="onDownload()" [icon]="faUser"> Ablauf für Lobpreisgruppe</app-button>
<app-button (click)="onDownloadHandout()" [icon]="faUsers"> Handout mit Copyright Infos</app-button>
</mat-menu>
</app-button-row>
}
</app-card>
</div>
}
</app-page-frame>

View File

@@ -1,6 +1,6 @@
:host {
--button-padding-mobile: 10px;
--button-font-size-mobile: 25px;
--button-padding-mobile: 4px;
--button-font-size-mobile: 20px;
}
.song-row:not(:last-child) {

View File

@@ -1,4 +1,12 @@
import {ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, HostListener, inject, OnDestroy, OnInit} from '@angular/core';
import {
ChangeDetectorRef,
Component,
CUSTOM_ELEMENTS_SCHEMA,
HostListener,
inject,
OnDestroy,
OnInit,
} from '@angular/core';
import {filter, map, shareReplay, switchMap, take, tap} from 'rxjs/operators';
import {ActivatedRoute, Router} from '@angular/router';
import {ShowService} from '../services/show.service';
@@ -55,6 +63,7 @@ import {BadgeComponent, BadgeType} from '../../../widget-modules/components/badg
import {ReportDialogComponent, ReportDialogSong} from '../dialog/report-dialog/report-dialog.component';
import {PublishedTypePipe} from '../../../widget-modules/pipes/published-type-translator/published-type.pipe';
import {ensureSwiperElement} from '../../../services/swiper-element';
import {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
@Component({
selector: 'app-show',
@@ -87,6 +96,7 @@ import {ensureSwiperElement} from '../../../services/swiper-element';
ReportedTypePipe,
PublishedTypePipe,
BadgeComponent,
PageFrameComponent,
],
})
export class ShowComponent implements OnInit, OnDestroy {
@@ -140,7 +150,7 @@ export class ShowComponent implements OnInit, OnDestroy {
shareReplay({
bufferSize: 1,
refCount: true,
})
}),
);
this.subs.push(
this.activatedRoute.params
@@ -148,12 +158,12 @@ export class ShowComponent implements OnInit, OnDestroy {
map(param => param as {showId: string}),
map(param => param.showId),
switchMap(showId => this.showSongService.list$(showId)),
filter(_ => !!_ && _.length > 0)
filter(_ => !!_ && _.length > 0),
)
.subscribe(_ => {
this.showSongs = _;
this.cRef.markForCheck();
})
}),
);
this.songs$ = this.show$.pipe(
@@ -161,7 +171,7 @@ export class ShowComponent implements OnInit, OnDestroy {
shareReplay({
bufferSize: 1,
refCount: true,
})
}),
);
}

View File

@@ -18,7 +18,7 @@
grid-template-areas: "keys title edit delete";
@media screen and (max-width: 860px) {
grid-template-columns: var(--song-key-column-width, 30px) auto 45px 45px;
grid-template-columns: var(--song-key-column-width, 30px) auto 30px 30px;
}
&.with-drag {
@@ -26,7 +26,7 @@
grid-template-areas: "drag keys title edit delete";
@media screen and (max-width: 860px) {
grid-template-columns: 24px var(--song-key-column-width, 30px) auto 45px 45px;
grid-template-columns: 24px var(--song-key-column-width, 30px) auto 30px 30px;
}
}
}
@@ -36,7 +36,7 @@
grid-template-areas: "title keys edit delete";
@media screen and (max-width: 860px) {
grid-template-columns: auto var(--song-key-column-width, 30px) 45px 45px;
grid-template-columns: auto var(--song-key-column-width, 30px) 30px 30px;
}
&.with-drag {
@@ -44,7 +44,7 @@
grid-template-areas: "drag title keys edit delete";
@media screen and (max-width: 860px) {
grid-template-columns: 24px auto var(--song-key-column-width, 30px) 45px 45px;
grid-template-columns: 24px auto var(--song-key-column-width, 30px) 30px 30px;
}
}
}
@@ -176,4 +176,5 @@ button {
textarea.edit {
font-family: 'Ubuntu Mono', monospace;
line-height: 15px;
}

View File

@@ -1,48 +1,50 @@
@if (songs$ | async; as songs) {
<app-sidebar>
<div sidebar class="sidebar-content">
<app-filter [songs]="songs"></app-filter>
</div>
<div content>
<app-card [padding]="false">
@for (song of songs; track trackBy($index, song)) {
<div [routerLink]="song.id" class="list-item">
<div class="number">{{ song.number }}</div>
<div class="title">
<span>{{ song.title }}</span>
@if (song.hasChordValidationIssues) {
<span class="validation-star" title="Akkord-Validierungsfehler vorhanden">*</span>
}
</div>
<div>
<ng-container *appRole="['contributor']">
@if (song.status === 'draft') {
<div class="warning">
<fa-icon [icon]="faDraft"></fa-icon>
<app-page-frame title="Lieder">
<div class="sidebar-content" sidebar>
<app-filter [songs]="songs"></app-filter>
</div>
<div content>
<app-card [padding]="false">
@for (song of songs; track trackBy($index, song)) {
<div [routerLink]="song.id" class="list-item">
<div class="number">{{ song.number }}</div>
<div class="title">
<span>{{ song.title }}</span>
@if (song.hasChordValidationIssues) {
<span class="validation-star" title="Akkord-Validierungsfehler vorhanden">*</span>
}
</div>
} @if (song.status === 'set') {
<div class="neutral">
<fa-icon [icon]="faDraft"></fa-icon>
<div>
<ng-container *appRole="['contributor']">
@if (song.status === 'draft') {
<div class="warning">
<fa-icon [icon]="faDraft"></fa-icon>
</div>
}
@if (song.status === 'set') {
<div class="neutral">
<fa-icon [icon]="faDraft"></fa-icon>
</div>
}
@if (song.status === 'final') {
<div class="success">
<fa-icon [icon]="faFinal"></fa-icon>
</div>
}
</ng-container>
@if (song.legalType === 'open') {
<div class="warning">
<fa-icon [icon]="faLegal"></fa-icon>
</div>
}
</div>
} @if (song.status === 'final') {
<div class="success">
<fa-icon [icon]="faFinal"></fa-icon>
</div>
}
</ng-container>
@if (song.legalType === 'open') {
<div class="warning">
<fa-icon [icon]="faLegal"></fa-icon>
<div>{{ song.key }}</div>
</div>
}
}
<div *appRole="['contributor']" class="list-action">
<app-button [fullWidth]="true" [icon]="faNewSong" routerLink="new">Neuen Song anlegen</app-button>
</div>
<div>{{ song.key }}</div>
</div>
}
<div *appRole="['contributor']" class="list-action">
<app-button [fullWidth]="true" [icon]="faNewSong" routerLink="new">Neuen Song anlegen</app-button>
</div>
</app-card>
</div>
</app-sidebar>
</app-card>
</div>
</app-page-frame>
}

View File

@@ -14,7 +14,7 @@ import {FilterComponent} from './filter/filter.component';
import {CardComponent} from '../../../widget-modules/components/card/card.component';
import {RoleDirective} from '../../../services/user/role.directive';
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {SidebarComponent} from '../../../widget-modules/components/sidebar/sidebar.component';
import {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
interface SongListItem extends Song {
@@ -27,7 +27,7 @@ interface SongListItem extends Song {
styleUrls: ['./song-list.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [fade],
imports: [FilterComponent, CardComponent, RouterLink, RoleDirective, FaIconComponent, AsyncPipe, SidebarComponent, ButtonComponent],
imports: [FilterComponent, CardComponent, RouterLink, RoleDirective, FaIconComponent, AsyncPipe, PageFrameComponent, ButtonComponent],
})
export class SongListComponent {
public faLegal = faBalanceScaleRight;
@@ -48,7 +48,7 @@ export class SongListComponent {
...song,
hasChordValidationIssues: this.textRenderingService.validateChordNotation(song.text ?? '').length > 0,
}));
})
}),
);
public trackBy = (index: number, show: SongListItem) => show.id;

View File

@@ -24,6 +24,7 @@
textarea {
font-family: 'Ubuntu Mono', monospace;
line-height: 15px;
}
}

View File

@@ -1,5 +1,7 @@
<div>
<app-edit-song></app-edit-song>
<app-edit-file></app-edit-file>
<app-history></app-history>
</div>
<app-page-frame title="Lieder" [withMenu]="false">
<div content>
<app-edit-song></app-edit-song>
<app-edit-file></app-edit-file>
<app-history></app-history>
</div>
</app-page-frame>

View File

@@ -2,12 +2,13 @@ import {Component, ViewChild} from '@angular/core';
import {EditSongComponent} from './edit-song/edit-song.component';
import {EditFileComponent} from './edit-file/edit-file.component';
import {HistoryComponent} from './history/history.component';
import {PageFrameComponent} from '../../../../widget-modules/components/sidebar/page-frame.component';
@Component({
selector: 'app-edit',
templateUrl: './edit.component.html',
styleUrls: ['./edit.component.less'],
imports: [EditSongComponent, EditFileComponent, HistoryComponent],
imports: [EditSongComponent, EditFileComponent, HistoryComponent, PageFrameComponent],
})
export class EditComponent {
@ViewChild(EditSongComponent) public editSongComponent: EditSongComponent | null = null;

View File

@@ -1,16 +1,18 @@
<app-card closeLink="../" heading="Neues Lied">
<div [formGroup]="form" class="split">
<mat-form-field appearance="outline">
<mat-label>Nummer</mat-label>
<input formControlName="number" matInput />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Titel</mat-label>
<input formControlName="title" matInput />
</mat-form-field>
</div>
<app-page-frame title="Lieder" [withMenu]="false">
<app-card closeLink="../" heading="Neues Lied" content>
<div [formGroup]="form" class="split">
<mat-form-field appearance="outline">
<mat-label>Nummer</mat-label>
<input formControlName="number" matInput />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Titel</mat-label>
<input formControlName="title" matInput />
</mat-form-field>
</div>
<app-button-row>
<app-button (click)="onSave()" [icon]="faSave">Anlegen</app-button>
</app-button-row>
</app-card>
<app-button-row>
<app-button (click)="onSave()" [icon]="faSave">Anlegen</app-button>
</app-button-row>
</app-card>
</app-page-frame>

View File

@@ -1,6 +1,5 @@
import {Component, OnInit, inject} from '@angular/core';
import {Component, DestroyRef, inject, OnInit} from '@angular/core';
import {faSave} from '@fortawesome/free-solid-svg-icons';
import {DestroyRef} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {SongService} from '../../services/song.service';
@@ -12,12 +11,13 @@ import {MatFormField, MatLabel} from '@angular/material/form-field';
import {MatInput} from '@angular/material/input';
import {ButtonRowComponent} from '../../../../widget-modules/components/button-row/button-row.component';
import {ButtonComponent} from '../../../../widget-modules/components/button/button.component';
import {PageFrameComponent} from '../../../../widget-modules/components/sidebar/page-frame.component';
@Component({
selector: 'app-new',
templateUrl: './new.component.html',
styleUrls: ['./new.component.less'],
imports: [CardComponent, ReactiveFormsModule, MatFormField, MatLabel, MatInput, ButtonRowComponent, ButtonComponent],
imports: [CardComponent, ReactiveFormsModule, MatFormField, MatLabel, MatInput, ButtonRowComponent, ButtonComponent, PageFrameComponent],
})
export class NewComponent implements OnInit {
private songService = inject(SongService);

View File

@@ -1,61 +1,77 @@
<div class="split">
@if (song$ | async; as song) {
<app-card [heading]="song.number + ' - ' + song.title" closeLink="../">
<div class="song">
<div>
<div *appRole="['leader', 'contributor']" class="detail">
<div>Typ: {{ song.type | songType }}</div>
<div>Tonart: {{ song.key }}</div>
<div>Tempo: {{ song.tempo }}</div>
<div>Status: {{ (song.status | status) || "entwurf" }}</div>
@if (song.legalOwner) {
<div>Rechteinhaber: {{ song.legalOwner | legalOwner }}</div>
} @if (song.legalOwnerId && song.legalOwner === 'CCLI') {
<app-page-frame title="Lieder" [withMenu]="false">
<div class="split" content>
@if (song$ | async; as song) {
<app-card [heading]="song.number + ' - ' + song.title" closeLink="../">
<div class="song">
<div>
<a href="https://songselect.ccli.com/Songs/{{ song.legalOwnerId }}" target="_blank"> CCLI Nummer: {{ song.legalOwnerId }} </a>
<div *appRole="['leader', 'contributor']" class="detail">
<div>Typ: {{ song.type | songType }}</div>
<div>Tonart: {{ song.key }}</div>
<div>Tempo: {{ song.tempo }}</div>
<div>Status: {{ (song.status | status) || "entwurf" }}</div>
@if (song.legalOwner) {
<div>Rechteinhaber: {{ song.legalOwner | legalOwner }}</div>
}
@if (song.legalOwnerId && song.legalOwner === 'CCLI') {
<div>
<a href="https://songselect.ccli.com/Songs/{{ song.legalOwnerId }}" target="_blank"> CCLI
Nummer: {{ song.legalOwnerId }} </a>
</div>
}
@if (song.legalOwnerId && song.legalOwner !== 'CCLI') {
<div>Rechteinhaber ID: {{ song.legalOwnerId }}</div>
}
@if (song.artist) {
<div>Künstler: {{ song.artist }}</div>
}
@if (song.label) {
<div>Verlag: {{ song.label }}</div>
}
@if (song.origin) {
<div>Quelle: {{ song.origin }}</div>
}
<div [matTooltip]="songUsageTooltip$ | async" matTooltipPosition="above">Wie oft
verwendet: {{ songCount$ | async }}
</div>
</div>
</div>
} @if (song.legalOwnerId && song.legalOwner !== 'CCLI') {
<div>Rechteinhaber ID: {{ song.legalOwnerId }}</div>
} @if (song.artist) {
<div>Künstler: {{ song.artist }}</div>
} @if (song.label) {
<div>Verlag: {{ song.label }}</div>
} @if (song.origin) {
<div>Quelle: {{ song.origin }}</div>
@if (user$ | async; as user) {
<app-song-text [chordMode]="user.chordMode" [showSwitch]="true" [text]="song.text"
[validateChordNotation]="true"></app-song-text>
}
<div [matTooltip]="songUsageTooltip$ | async" matTooltipPosition="above">Wie oft verwendet: {{ songCount$ | async }}</div>
<mat-chip-listbox *appRole="['leader', 'contributor']" aria-label="Attribute">
@for (flag of getFlags(song.flags); track flag) {
<mat-chip-option>{{ flag }}</mat-chip-option>
}
</mat-chip-listbox>
<div *appRole="['leader', 'contributor']" class="text">{{ song.comment }}</div>
</div>
</div>
@if (user$ | async; as user) {
<app-song-text [chordMode]="user.chordMode" [showSwitch]="true" [text]="song.text" [validateChordNotation]="true"></app-song-text>
}
<mat-chip-listbox *appRole="['leader', 'contributor']" aria-label="Attribute">
@for (flag of getFlags(song.flags); track flag) {
<mat-chip-option>{{ flag }} </mat-chip-option>
}
</mat-chip-listbox>
<div *appRole="['leader', 'contributor']" class="text">{{ song.comment }}</div>
</div>
<app-button-row>
<app-button (click)="onDelete(song.id)" *appRole="['admin']" [icon]="faDelete">Löschen </app-button>
<app-button *appRole="['contributor']" [icon]="faEdit" routerLink="edit">Bearbeiten </app-button>
<ng-container *appRole="['leader']">
<app-button [icon]="faFileCirclePlus" [matMenuTriggerFor]="menu"> Zu Veranstaltung hinzufügen </app-button>
<mat-menu #menu="matMenu">
@for (show of privateShows$|async; track show.id) {
<app-button (click)="addSongToShow(show, song)"> {{ show.date.toDate() | date: "dd.MM.yyyy" }} {{ show.showType | showType }} </app-button>
}
</mat-menu>
</ng-container>
</app-button-row>
</app-card>
} @if (files$ | async; as files) { @if (files.length > 0) {
<app-card heading="Anhänge">
@for (file of files$ | async; track file.id) {
<p>
<app-file [file]="file"></app-file>
</p>
<app-button-row>
<app-button (click)="onDelete(song.id)" *appRole="['admin']" [icon]="faDelete">Löschen</app-button>
<app-button *appRole="['contributor']" [icon]="faEdit" routerLink="edit">Bearbeiten</app-button>
<ng-container *appRole="['leader']">
<app-button [icon]="faFileCirclePlus" [matMenuTriggerFor]="menu"> Zu Veranstaltung hinzufügen</app-button>
<mat-menu #menu="matMenu">
@for (show of privateShows$|async; track show.id) {
<app-button
(click)="addSongToShow(show, song)"> {{ show.date.toDate() | date: "dd.MM.yyyy" }} {{ show.showType | showType }}
</app-button>
}
</mat-menu>
</ng-container>
</app-button-row>
</app-card>
}
</app-card>
} }
</div>
@if (files$ | async; as files) {
@if (files.length > 0) {
<app-card heading="Anhänge">
@for (file of files$ | async; track file.id) {
<p>
<app-file [file]="file"></app-file>
</p>
}
</app-card>
}
}
</div>
</app-page-frame>

View File

@@ -1,4 +1,4 @@
import {Component, OnInit, inject} from '@angular/core';
import {Component, inject, OnInit} from '@angular/core';
import {ActivatedRoute, Router, RouterLink} from '@angular/router';
import {SongService} from '../services/song.service';
import {distinctUntilChanged, map, switchMap} from 'rxjs/operators';
@@ -26,6 +26,7 @@ import {LegalOwnerPipe} from '../../../widget-modules/pipes/legal-owner-translat
import {StatusPipe} from '../../../widget-modules/pipes/status-translater/status.pipe';
import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/show-type.pipe';
import {MatTooltip} from '@angular/material/tooltip';
import {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
@Component({
selector: 'app-song',
@@ -50,6 +51,7 @@ import {MatTooltip} from '@angular/material/tooltip';
StatusPipe,
ShowTypePipe,
MatTooltip,
PageFrameComponent,
],
})
export class SongComponent implements OnInit {
@@ -84,14 +86,14 @@ export class SongComponent implements OnInit {
const song$ = this.activatedRoute.params.pipe(
map(param => param as {songId: string}),
map(param => param.songId),
switchMap(songId => this.songService.read$(songId))
switchMap(songId => this.songService.read$(songId)),
);
this.song$ = song$;
this.files$ = this.activatedRoute.params.pipe(
map(param => param as {songId: string}),
map(param => param.songId),
switchMap(songId => this.fileService.read$(songId))
switchMap(songId => this.fileService.read$(songId)),
);
this.songCount$ = combineLatest([this.userService.user$, song$]).pipe(
@@ -102,7 +104,7 @@ export class SongComponent implements OnInit {
return user?.songUsage?.[song.id] ?? 0;
}),
distinctUntilChanged()
distinctUntilChanged(),
);
this.songUsageShows$ = combineLatest([this.userService.user$, this.showService.list$(), song$]).pipe(
@@ -115,7 +117,7 @@ export class SongComponent implements OnInit {
.filter(show => show.owner === user.id)
.filter(show => (show.songIds ?? []).includes(song.id))
.sort((a, b) => b.date.toMillis() - a.date.toMillis());
})
}),
);
this.songUsageTooltip$ = combineLatest([this.songCount$, this.songUsageShows$]).pipe(
@@ -129,7 +131,7 @@ export class SongComponent implements OnInit {
}
return shows.map(show => `${this.dateFormatter.format(show.date.toDate())} - ${this.showTypePipe.transform(show.showType)}`).join('\n');
})
}),
);
}

View File

@@ -1,25 +1,30 @@
@if (user$ | async; as user) {
<app-card heading="Hallo {{ user.name }}">
<p>
@if (getUserRoles(user.role).length === 0) {
<span class="warn">Es wurden noch keine Berechtigungen zugeteilt, bitte wende Dich an den Administrator!</span>
<app-page-frame title="Benutzer" [withMenu]="false">
<div content>
@if (user$ | async; as user) {
<app-card heading="Hallo {{ user.name }}">
<p>
@if (getUserRoles(user.role).length === 0) {
<span
class="warn">Es wurden noch keine Berechtigungen zugeteilt, bitte wende Dich an den Administrator!</span>
}
<span>{{ transdormUserRoles(user.role) }}</span>
</p>
<mat-form-field appearance="outline">
<mat-label>bevorzugte Anzeige der Akkorde</mat-label>
<mat-select (ngModelChange)="onChordModeChanged(user.id, $event)" [ngModel]="user.chordMode">
<mat-option [value]="null"></mat-option>
<mat-option value="hide">nur den Liedtext anzeigen</mat-option>
<mat-option value="onlyFirst">in Strophen die Akkorde nur für die erste anzeigen</mat-option>
<mat-option value="show">alle anzeigen</mat-option>
</mat-select>
<mat-hint>Das ist nur die Voreinstellung, die Anzeige kann für jedes Lied geändert werden.</mat-hint>
</mat-form-field>
<app-button-row>
<app-button [icon]="faSignOut" routerLink="../logout">Abmelden</app-button>
</app-button-row>
</app-card>
}
<span>{{ transdormUserRoles(user.role) }}</span>
</p>
<mat-form-field appearance="outline">
<mat-label>bevorzugte Anzeige der Akkorde</mat-label>
<mat-select (ngModelChange)="onChordModeChanged(user.id, $event)" [ngModel]="user.chordMode">
<mat-option [value]="null"></mat-option>
<mat-option value="hide">nur den Liedtext anzeigen</mat-option>
<mat-option value="onlyFirst">in Strophen die Akkorde nur für die erste anzeigen </mat-option>
<mat-option value="show">alle anzeigen</mat-option>
</mat-select>
<mat-hint>Das ist nur die Voreinstellung, die Anzeige kann für jedes Lied geändert werden. </mat-hint>
</mat-form-field>
<app-button-row>
<app-button [icon]="faSignOut" routerLink="../logout">Abmelden</app-button>
</app-button-row>
</app-card>
}
<app-users *appRole="['admin']"></app-users>
<app-users *appRole="['admin']"></app-users>
</div>
</app-page-frame>

View File

@@ -1,4 +1,4 @@
import {Component, OnInit, inject} from '@angular/core';
import {Component, inject, OnInit} from '@angular/core';
import {UserService} from '../../../services/user/user.service';
import {Observable} from 'rxjs';
import {User} from '../../../services/user/user';
@@ -17,6 +17,7 @@ import {ButtonComponent} from '../../../widget-modules/components/button/button.
import {RouterLink} from '@angular/router';
import {RoleDirective} from '../../../services/user/role.directive';
import {UsersComponent} from './users/users.component';
import {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
@Component({
selector: 'app-info',
@@ -37,6 +38,7 @@ import {UsersComponent} from './users/users.component';
RoleDirective,
UsersComponent,
AsyncPipe,
PageFrameComponent,
],
})
export class InfoComponent implements OnInit {

View File

@@ -21,7 +21,7 @@ nav {
&.hidden {
@media screen and (max-width: 860px) {
top: -60px;
// top: -60px;
}
}

View File

@@ -1,6 +1,11 @@
.row {
display: flex;
flex-direction: row-reverse;
@media screen and (max-width: 860px) {
flex-direction: column-reverse;
}
gap: var(--gap-m);
width: 100%;
flex-wrap: wrap;
padding-top: var(--gap-l);
}

View File

@@ -1,6 +1,6 @@
<button [disabled]="disabled" mat-button>
<button [disabled]="disabled">
@if (icon) {
<span><fa-icon [icon]="icon"></fa-icon><span class="content">&nbsp;</span></span>
<span><fa-icon [icon]="icon"></fa-icon><span class="content">&nbsp;</span></span>
}
<span class="button-content">
<ng-content></ng-content>

View File

@@ -1,15 +1,19 @@
:host {
display: inline-flex;
}
:host(.full-width) {
display: flex;
width: 100%;
}
button {
color: var(--text);
display: flex;
color: var(--primary-color);
transition: var(--transition);
border-radius: 4px;
font-size: 1rem;
background: var(--surface-strong);
border: 1px solid var(--primary-color);
padding: var(--gap-s);
cursor: pointer;
:host(.full-width) & {
width: 100%;
@@ -18,18 +22,19 @@ button {
&:hover {
color: var(--primary-active);
background: var(--surface-subtle);
}
@media screen and (max-width: 860px) {
font-size: 30px;
font-size: 1.2rem;
width: 100%;
justify-content: center;
.button-content {
flex-grow: 1;
}
}
}
.button-content {
@media screen and (max-width: 860px) {
display: none ;
}
}
fa-icon {
width: 20px;

View File

@@ -1,6 +1,5 @@
import {Component, Input} from '@angular/core';
import {IconProp} from '@fortawesome/fontawesome-svg-core';
import {MatButton} from '@angular/material/button';
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
@@ -8,7 +7,7 @@ import {FaIconComponent} from '@fortawesome/angular-fontawesome';
selector: 'app-button',
templateUrl: './button.component.html',
styleUrls: ['./button.component.less'],
imports: [MatButton, FaIconComponent],
imports: [FaIconComponent],
host: {
'[class.full-width]': 'fullWidth',
},

View File

@@ -1,12 +1,14 @@
<div @fade [class.fullscreen]="fullscreen" [class.padding]="padding" class="card">
@if (closeLink && !fullscreen) {
<button [routerLink]="closeLink" class="btn-close" mat-icon-button>
<fa-icon [icon]="closeIcon"></fa-icon>
</button>
} @if (heading && !fullscreen) {
<div class="heading">{{ heading }}</div>
} @if (subheading && !fullscreen) {
<div class="subheading">{{ subheading }}</div>
<button [routerLink]="closeLink" class="btn-close" mat-icon-button>
<fa-icon [icon]="closeIcon"></fa-icon>
</button>
}
@if (heading && !fullscreen) {
<div class="heading">{{ heading }}</div>
}
@if (subheading && !fullscreen) {
<div class="subheading">{{ subheading }}</div>
}
<ng-content></ng-content>
</div>

View File

@@ -1,7 +1,7 @@
@import "../../../../styles/shadow";
.card {
margin: 20px;
margin: var(--gap-l) var(--gap-l) 0;
border-radius: 8px;
background: var(--surface);
backdrop-filter: blur(15px);
@@ -10,19 +10,17 @@
width: 800px;
position: relative;
color: var(--text);
padding: 10px 0;
padding: var(--gap-m) 0;
box-shadow: var(--shadow-card-3);
@media screen and (max-width: 860px) {
width: 100vw;
border-radius: 0;
background: var(--surface-strong);
margin: 0;
width: calc(100vw - 10px);
margin: 5px;
color: var(--text);
}
&.padding {
padding: 20px;
padding: var(--gap-l);
}
box-sizing: border-box;

View File

@@ -0,0 +1,27 @@
@if (withMenu() || title()) {
<div class="header">
@if (withMenu()) {
<button (click)="toggle()"
[attr.aria-expanded]="!collapsed"
aria-label="Sidebar umschalten"
class="sidebar-toggle"
mat-icon-button type="button">
<fa-icon [icon]="collapsed ? closedIcon : openIcon"></fa-icon>
</button>
}
<div class="title">{{ title() }}</div>
</div>
}
@if (!collapsed) {
<button (click)="close()" aria-label="Sidebar schließen" class="sidebar-backdrop" tabindex="-1"
type="button"></button>
}
<aside [class.collapsed]="collapsed">
<div aria-hidden="true" class="sidebar-toggle-placeholder"></div>
<div class="sidebar-body">
<ng-content select="[sidebar]"></ng-content>
</div>
</aside>
<div class="content">
<ng-content select="[content]"></ng-content>
</div>

View File

@@ -19,20 +19,20 @@
.sidebar-toggle {
--icon-button-color: var(--primary-hover);
--icon-button-hover-color: var(--primary-active);
--icon-button-hover-color: var(--primary);
position: fixed;
top: calc(50px + var(--sidebar-toggle-offset));
left: var(--sidebar-toggle-offset);
z-index: 11;
color: var(--icon-button-color);
background: var(--surface-strong);
box-shadow: none;
border-radius: 999px;
transition: all 150ms ease-in-out;
}
:host.collapsed .sidebar-toggle {
--icon-button-color: var(--text);
--icon-button-hover-color: var(--primary-active);
--icon-button-color: var(--surface);
--icon-button-hover-color: var(--surface-subtle);
}
.sidebar-toggle fa-icon {
@@ -92,7 +92,7 @@ aside.collapsed {
.sidebar-toggle {
top: calc(50px + 8px);
left: 12px;
left: 5px;
right: auto;
z-index: 13;
}
@@ -121,6 +121,32 @@ aside.collapsed {
.content {
grid-column: auto;
width: 100%;
padding-top: calc(var(--sidebar-toggle-size) + 6px);
}
}
.header {
@media screen and (max-width: 860px) {
height: 50px;
}
}
.title {
color: var(--surface);
font-size: 30px;
text-transform: uppercase;
text-align: right;
opacity: 0.5;
text-shadow: var(--shadow-card-3);
position: fixed;
right: 10px;
top: 61px;
transition: all 150ms ease-in-out;
@media screen and (min-width: 888px) {
transform-origin: top right;
transform: rotate(90deg) translateX(100%) translateX(10px);
text-shadow: 10px 0 20px rgba(0, 0, 0, 0.19), 6px 0 6px rgba(0, 0, 0, 0.23);
}
}

View File

@@ -1,17 +1,17 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {SidebarComponent} from './sidebar.component';
import {PageFrameComponent} from './page-frame.component';
describe('SidebarComponent', () => {
let component: SidebarComponent;
let fixture: ComponentFixture<SidebarComponent>;
let component: PageFrameComponent;
let fixture: ComponentFixture<PageFrameComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SidebarComponent],
imports: [PageFrameComponent],
}).compileComponents();
fixture = TestBed.createComponent(SidebarComponent);
fixture = TestBed.createComponent(PageFrameComponent);
component = fixture.componentInstance;
await fixture.whenStable();
});

View File

@@ -1,21 +1,23 @@
import {Component} from '@angular/core';
import {Component, input} from '@angular/core';
import {MatIconButton} from '@angular/material/button';
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {faBars, faChevronLeft} from '@fortawesome/free-solid-svg-icons';
@Component({
selector: 'app-sidebar',
selector: 'app-page-frame',
imports: [MatIconButton, FaIconComponent],
templateUrl: './sidebar.component.html',
styleUrl: './sidebar.component.less',
templateUrl: './page-frame.component.html',
styleUrl: './page-frame.component.less',
host: {
'[class.collapsed]': 'collapsed',
},
})
export class SidebarComponent {
export class PageFrameComponent {
public collapsed = true;
public openIcon = faChevronLeft;
public closedIcon = faBars;
public title = input.required<string>();
public withMenu = input<boolean>(true);
public toggle(): void {
this.collapsed = !this.collapsed;

View File

@@ -1,15 +0,0 @@
<button (click)="toggle()" [attr.aria-expanded]="!collapsed" aria-label="Sidebar umschalten" class="sidebar-toggle" mat-icon-button type="button">
<fa-icon [icon]="collapsed ? closedIcon : openIcon"></fa-icon>
</button>
@if (!collapsed) {
<button (click)="close()" aria-label="Sidebar schließen" class="sidebar-backdrop" tabindex="-1" type="button"></button>
}
<aside [class.collapsed]="collapsed">
<div aria-hidden="true" class="sidebar-toggle-placeholder"></div>
<div class="sidebar-body">
<ng-content select="[sidebar]"></ng-content>
</div>
</aside>
<div class="content">
<ng-content select="[content]"></ng-content>
</div>

View File

@@ -14,8 +14,10 @@
--text-soft: #7a858c;
--text-inverse: #f7fbff;
--color-primary-dark: #48686e;
--color-primary: #6f8f95;
--color-primary-light: #85a4aa;
--primary-color-sat: #578f9a;
--primary-color: #6f8f95;
--primary-hover: #85a4aa;
--primary-active: #5b797e;
@@ -41,6 +43,11 @@
--mat-dialog-supporting-text-color: var(--text);
--mat-button-text-label-text-color: var(--color-primary-dark);
--gap-l: 20px;
--gap-m: calc(var(--gap-l) / 1.618);
--gap-s: calc(var(--gap-m) / 1.618);
}
html {