add invites

This commit is contained in:
_Bastler 2021-05-06 21:26:57 +02:00
parent 3e02cbb353
commit c8a10eb73c
12 changed files with 1213 additions and 892 deletions

View File

@ -23,6 +23,7 @@ import {UserComponent} from './pages/user/user.component'
import {JitsiComponent} from './pages/jitsi/jitsi.component' import {JitsiComponent} from './pages/jitsi/jitsi.component'
import {AliasesComponent} from './pages/account/aliases/aliases.component'; import {AliasesComponent} from './pages/account/aliases/aliases.component';
import {DomainsComponent} from './pages/account/domains/domains.component'; import {DomainsComponent} from './pages/account/domains/domains.component';
import {InvitesComponent} from './pages/invites/invites.component';
const routes: Routes = [ const routes: Routes = [
{path: '', redirectTo: "/services", pathMatch: 'full'}, {path: '', redirectTo: "/services", pathMatch: 'full'},
@ -50,13 +51,14 @@ const routes: Routes = [
{path: 'register', component: RegisterComponent, canActivate: [AnonymousGuard]}, {path: 'register', component: RegisterComponent, canActivate: [AnonymousGuard]},
{path: 'tokens', component: TokensComponent, canActivate: [AuthGuard]}, {path: 'tokens', component: TokensComponent, canActivate: [AuthGuard]},
{path: 'jitsi', component: JitsiComponent, canActivate: [AuthenticatedGuard]}, {path: 'jitsi', component: JitsiComponent, canActivate: [AuthenticatedGuard]},
{path: 'invites/:quota', component: InvitesComponent, canActivate: [AuthenticatedGuard]},
{path: 'unavailable', component: UnavailableComponent}, {path: 'unavailable', component: UnavailableComponent},
{path: 'p/:username', component: UserComponent, canActivate: [AuthUpdateGuard]}, {path: 'p/:username', component: UserComponent, canActivate: [AuthUpdateGuard]},
{path: '**', component: NotfoundComponent, pathMatch: 'full', canActivate: [AuthUpdateGuard]}, {path: '**', component: NotfoundComponent, pathMatch: 'full', canActivate: [AuthUpdateGuard]},
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes, { onSameUrlNavigation: 'reload', relativeLinkResolution: 'legacy' })], imports: [RouterModule.forRoot(routes, {onSameUrlNavigation: 'reload', relativeLinkResolution: 'legacy'})],
exports: [RouterModule] exports: [RouterModule]
}) })
export class AppRoutingModule {} export class AppRoutingModule {}

View File

@ -19,6 +19,7 @@ import {LoginTotpComponent} from './pages/login-totp/login-totp.component';
import {FormLoginComponent} from './pages/form-login/form-login.component'; import {FormLoginComponent} from './pages/form-login/form-login.component';
import {FormLoginTotpComponent} from './pages/form-login-totp/form-login-totp.component'; import {FormLoginTotpComponent} from './pages/form-login-totp/form-login-totp.component';
import {TokensComponent} from './pages/tokens/tokens.component'; import {TokensComponent} from './pages/tokens/tokens.component';
import {InvitesComponent} from './pages/invites/invites.component';
import {PermissionsComponent} from './ui/permissions/permissions.component'; import {PermissionsComponent} from './ui/permissions/permissions.component';
import {ProfileFieldDialog, ProfileFieldsComponent, ProfileFieldBlob} from './ui/profilefields/profilefields.component'; import {ProfileFieldDialog, ProfileFieldsComponent, ProfileFieldBlob} from './ui/profilefields/profilefields.component';
import {QuotasComponent} from './ui/quotas/quotas.component'; import {QuotasComponent} from './ui/quotas/quotas.component';
@ -70,6 +71,7 @@ export class XhrInterceptor implements HttpInterceptor {
FormLoginComponent, FormLoginComponent,
FormLoginTotpComponent, FormLoginTotpComponent,
TokensComponent, TokensComponent,
InvitesComponent,
ServicesComponent, ServicesComponent,
PermissionsComponent, PermissionsComponent,
ProfileFieldsComponent, ProfileFieldDialog, ProfileFieldBlob, ProfileFieldsComponent, ProfileFieldDialog, ProfileFieldBlob,

View File

@ -0,0 +1,63 @@
<h3>{{'invites' | i18n}}</h3>
<table mat-table matSort [dataSource]="invites">
<ng-container matColumnDef="starts">
<th mat-header-cell *matHeaderCellDef> {{'invite.starts' | i18n}} </th>
<td mat-cell *matCellDef="let invite"> {{ invite.starts | date:datetimeformat}} </td>
</ng-container>
<ng-container matColumnDef="expires">
<th mat-header-cell *matHeaderCellDef> {{'invite.expires' | i18n}} </th>
<td mat-cell *matCellDef="let invite"> {{ invite.expires | date:datetimeformat}} </td>
</ng-container>
<ng-container matColumnDef="link">
<th mat-header-cell *matHeaderCellDef> {{'invite.link' | i18n}} </th>
<td mat-cell *matCellDef="let invite"> <a href="{{ invite.codeLink}}" target="_blank" mat-button color="accent">
{{
invite.code}}</a> </td>
</ng-container>
<ng-container matColumnDef="note">
<th mat-header-cell *matHeaderCellDef> {{'invite.note' | i18n}} </th>
<td mat-cell *matCellDef="let invite"> {{ invite.note}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="inviteColumns"></tr>
<tr mat-row *matRowDef="let myRowData; columns: inviteColumns"></tr>
</table>
<mat-card>
<mat-card-content>
<p>{{'invites.info' | i18n}}</p>
<p *ngIf="!inviteQuota">{{'invites.noQuota' | i18n}}</p>
<div *ngIf="inviteQuota">
<p>{{'invites.left' | i18n:inviteQuota}}</p>
</div>
</mat-card-content>
<mat-card-actions>
<button *ngIf="inviteQuota && !working" mat-raised-button color="primary" (click)="create()">
{{'invite.create' | i18n}}
</button>
</mat-card-actions>
</mat-card>
<div *ngIf="others && others.content">
<h4>{{'invites.others' | i18n}}</h4>
<mat-form-field>
<input matInput [formControl]="searchFormControl" placeholder="{{'invites.search' | i18n}}">
</mat-form-field>
<table mat-table matSort [dataSource]="others.content">
<ng-container matColumnDef="note">
<th mat-header-cell *matHeaderCellDef> {{'invite.note' | i18n}} </th>
<td mat-cell *matCellDef="let invite"> {{ invite.note}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="otherColumns"></tr>
<tr mat-row *matRowDef="let myRowData; columns: otherColumns"></tr>
</table>
<mat-paginator [pageSizeOptions]="pageSizeOptions" [length]="others.totalElements" [pageSize]="others.size" (page)="updateOthers($event)" showFirstLastButtons></mat-paginator>
</div>

View File

@ -0,0 +1,3 @@
mat-form-field {
display: block;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InvitesComponent } from './invites.component';
describe('InvitesComponent', () => {
let component: InvitesComponent;
let fixture: ComponentFixture<InvitesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ InvitesComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(InvitesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,99 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {PageEvent} from '@angular/material/paginator';
import {AuthService} from '../../services/auth.service';
import {I18nService} from '../../services/i18n.service';
import {QuotaService} from '../../services/quota.service';
import {InviteService} from '../../services/invites.service';
import {FormControl} from '@angular/forms';
import {debounceTime} from 'rxjs/operators';
@Component({
selector: 'app-invites',
templateUrl: './invites.component.html',
styleUrls: ['./invites.component.scss']
})
export class InvitesComponent implements OnInit {
quota: string;
invites: any[];
others: any;
inviteQuota: number = 0;
success: boolean;
working: boolean;
datetimeformat: string;
pageSizeOptions: number[] = [5, 10, 25, 50];
searchFormControl = new FormControl();
inviteColumns = ["starts", "expires", "link", "note"];
otherColumns = ["note"];
constructor(
private authService: AuthService,
private inviteService: InviteService,
private i18n: I18nService,
private quotaService: QuotaService,
private router: Router,
private route: ActivatedRoute) {
}
async ngOnInit() {
this.datetimeformat = this.i18n.get('format.datetime', []);
this.quota = this.route.snapshot.paramMap.get('quota');
this.searchFormControl.valueChanges.pipe(debounceTime(500)).subscribe(value => {
this.inviteService.getOthersPages(this.quota, 0, this.others.size, value).subscribe((data: any) => {
this.others = data;
}, (error) => {})
})
this.update();
}
update(): void {
this.inviteQuota = 0;
this.quotaService.quotas().subscribe((data: any) => {
for(let quota of data) {
if(quota.name == "invite_" + this.quota) {
this.inviteQuota = quota.value;
}
}
})
this.inviteService.get(this.quota).subscribe((data: any) => {
this.invites = data;
})
this.inviteService.getOthers(this.quota).subscribe((data: any) => {
this.others = data;
}, (error) => {})
}
create(): void {
this.working = true;
this.inviteService.create(this.quota, {}).subscribe(response => {
this.update();
this.working = false;
}, (error) => {
this.working = false;
if(error.status == 409) {
let errors = {};
for(let code of error.error) {
errors[code.field] = errors[code.field] || {};
errors[code.field][code.code] = true;
}
}
})
}
updateOthers(event: PageEvent) {
this.inviteService.getOthersPages(this.quota, event.pageIndex, event.pageSize, "").subscribe((data: any) => {
this.others = data;
}, (error) => {})
}
}

View File

@ -2,24 +2,34 @@
<p *ngIf="!services || services.length == 0">{{'services.empty' | i18n}}</p> <p *ngIf="!services || services.length == 0">{{'services.empty' | i18n}}</p>
<div fxLayout="row wrap" fxLayoutGap="16px grid">
<div fxFlex="33.33%" fxFlex.sm="50%" fxFlex.xs="100%" *ngFor="let service of services"> <table *ngIf="services && services.length > 0" mat-table matSort [dataSource]="services" (matSortChange)="sortData($event)" matSortActive="name" matSortDirection="desc">
<mat-card>
<mat-card-header> <ng-container matColumnDef="icon">
<th mat-header-cell *matHeaderCellDef> </th>
<td mat-cell *matCellDef="let service">
<mat-icon>{{'services.' + service.name + '.icon' | i18n}}</mat-icon> <mat-icon>{{'services.' + service.name + '.icon' | i18n}}</mat-icon>
<mat-card-title>{{'services.' + service.name + '.title' | i18n}}</mat-card-title> </td>
<mat-card-subtitle>{{'services.' + service.name + '.subtitle' | i18n}}</mat-card-subtitle> </ng-container>
</mat-card-header>
<mat-card-content>
<p> <ng-container matColumnDef="name">
{{ 'services.' + service.name + '.text' | i18n}} <th mat-header-cell *matHeaderCellDef mat-sort-header="name"> {{'service.name' | i18n}} </th>
</p> <td mat-cell *matCellDef="let service" class="text-center">
</mat-card-content> <a href="{{service.url}}" [target]="service.sameSite ? '_self' : '_blank'" mat-button color="accent">
<mat-card-actions> <strong>{{'services.' + service.name + '.title' | i18n}}</strong>
<a href="{{service.url}}" [target]="service.sameSite ? '_self' : '_blank'" mat-raised-button <mat-icon *ngIf="!service.sameSite" inline="true">
color="accent">{{'services.goto' | i18n}} <mat-icon *ngIf="!service.sameSite" inline="true"> open_in_new</mat-icon>
open_in_new</mat-icon></a> </a>
</mat-card-actions> <div><small>{{'services.' + service.name + '.subtitle' | i18n}}</small></div>
</mat-card> </td>
</div> </ng-container>
</div>
<ng-container matColumnDef="text">
<th mat-header-cell *matHeaderCellDef> {{'service.text' | i18n}} </th>
<td mat-cell *matCellDef="let service">{{ 'services.' + service.name + '.text' | i18n}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="serviceColumns"></tr>
<tr mat-row *matRowDef="let myRowData; columns: serviceColumns"></tr>
</table>

View File

@ -1,5 +1,7 @@
import { Component, OnInit, Input } from '@angular/core'; import {Component, OnInit} from '@angular/core';
import { ServiceService } from '../../services/service.service'; import {Sort} from '@angular/material/sort';
import {ServiceService} from '../../services/service.service';
import {I18nService} from '../../services/i18n.service';
@Component({ @Component({
selector: 'app-services', selector: 'app-services',
@ -9,13 +11,34 @@ import { ServiceService } from '../../services/service.service';
export class ServicesComponent implements OnInit { export class ServicesComponent implements OnInit {
services = []; services = [];
serviceColumns = ["icon", "name", "text"];
constructor(private serviceService: ServiceService) { } constructor(private serviceService: ServiceService, private i18n: I18nService) {}
ngOnInit(): void { ngOnInit(): void {
this.serviceService.services().subscribe((data: any) => { this.serviceService.services().subscribe((data: any) => {
this.services = data; this.services = data;
this.sortData({"active": "name", "direction": "desc"});
}) })
} }
sortData(sort: Sort) {
const data = this.services.slice();
if(!sort.active || sort.direction === '') {
this.services = data;
return;
}
this.services = data.sort((a, b) => {
const isAsc = sort.direction === 'asc';
switch(sort.active) {
case 'name': return this.compare(this.i18n.get('services.' + a.name + '.title', []), this.i18n.get('services.' + b.name + '.title', []), isAsc);
default: return 0;
}
});
}
compare(a: number | string | String, b: number | string | String, isAsc: boolean) {
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}
} }

View File

@ -0,0 +1,35 @@
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {environment} from '../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class InviteService {
constructor(private http: HttpClient) {
}
get(quota: string) {
return this.http.get(environment.apiUrl + "/invites" + (quota ? "?quota=" + quota : ""));
}
getOthers(quota: string) {
return this.http.get(environment.apiUrl + "/invites/" + quota + "/others");
}
getOthersPages(quota: string, page : number, size : number, search : string) {
return this.http.get(environment.apiUrl + "/invites/" + quota + "/others?page=" + page + "&size=" + size + "&search=" + search);
}
create(quota: string, invite: any) {
return this.http.post(environment.apiUrl + "/invites/" + quota, invite);
}
update(invite: any) {
return this.http.post(environment.apiUrl + "/invites", invite);
}
}

View File

@ -1,5 +1,4 @@
<table mat-table matSort [dataSource]="profileFields" (matSortChange)="sortData($event)" matSortActive="index" <table mat-table matSort [dataSource]="profileFields" (matSortChange)="sortData($event)" matSortActive="index" matSortDirection="asc">
matSortDirection="asc">
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header="name"> {{'profileField.name' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header="name"> {{'profileField.name' | i18n}} </th>
@ -15,11 +14,9 @@
<span *ngSwitchCase="'DATETIME'">{{profileField.value | date:datetimeformat}}</span> <span *ngSwitchCase="'DATETIME'">{{profileField.value | date:datetimeformat}}</span>
<span *ngSwitchCase="'TIME'">{{profileField.value | date:timeformat}}</span> <span *ngSwitchCase="'TIME'">{{profileField.value | date:timeformat}}</span>
<a *ngSwitchCase="'URL'" class="accent" href="{{profileField.value}}">{{profileField.value}}</a> <a *ngSwitchCase="'URL'" class="accent" href="{{profileField.value}}">{{profileField.value}}</a>
<a *ngSwitchCase="'EMAIL'" class="accent" <a *ngSwitchCase="'EMAIL'" class="accent" href="mailto:{{profileField.value}}">{{profileField.value}}</a>
href="mailto:{{profileField.value}}">{{profileField.value}}</a>
<span *ngSwitchCase="'NUMBER'">{{profileField.value}}</span> <span *ngSwitchCase="'NUMBER'">{{profileField.value}}</span>
<button *ngSwitchCase="'BLOB'" mat-raised-buttonu <button *ngSwitchCase="'BLOB'" mat-raised-button (click)="openBlob(profileField)">{{'profileField.openBlob' | i18n}}</button>
(click)="openBlob(profileField)">{{'profileField.openBlob' | i18n}}</button>
<mat-slide-toggle *ngSwitchCase="'BOOL'" [checked]="profileField.value == 'true'" disabled> <mat-slide-toggle *ngSwitchCase="'BOOL'" [checked]="profileField.value == 'true'" disabled>
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>

View File

@ -27,6 +27,21 @@
"info": { "info": {
".": "Info" ".": "Info"
}, },
"invite": {
".": "Einleidung",
"create": "Einladung erstellen",
"expires": "Gültig bis",
"link": "Link",
"note": "Notiz",
"starts": "Gültig ab"
},
"invites": {
".": "Einladungen",
"info": "Hier kannst du neue Einladungen erstellen. Um die Einladung zu bearbeiten klicke einfach auf den Link. Wenn du authoriziert bist, kannst du dort direkt den persönlichen Einladungstext bearbeiten oder eine Notiz hinzufügen.",
"left": "Du kannst noch {0} Einladungen erstellen.",
"others": "Einladungen anderer Mitglieder",
"search": "Suche"
},
"jitsi": { "jitsi": {
"rooms": { "rooms": {
".": "Jitsi Räume", ".": "Jitsi Räume",
@ -35,7 +50,7 @@
"delete": "Löschen", "delete": "Löschen",
"error": { "error": {
"expires": "Ungültiges Ende.", "expires": "Ungültiges Ende.",
"moderationStarts" : "Ungültiger Beginn Moderation. Moderation muss vor Beginn liegen.", "moderationStarts": "Ungültiger Beginn Moderation. Moderation muss vor Beginn liegen.",
"room": "Bitte gebe einen anderen Namen an. Der Name ist schon vergeben oder enthält ungültige Zeichen. Erlaubt sind nur Buchstaben und Zahlen.", "room": "Bitte gebe einen anderen Namen an. Der Name ist schon vergeben oder enthält ungültige Zeichen. Erlaubt sind nur Buchstaben und Zahlen.",
"starts": "Ungültiger Beginn." "starts": "Ungültiger Beginn."
}, },
@ -43,9 +58,9 @@
"info": "Du kannst hier Jitsi Räume erstellen. Die Anzahl wird über eine Quota begrenzt.", "info": "Du kannst hier Jitsi Räume erstellen. Die Anzahl wird über eine Quota begrenzt.",
"left": "Du kannst noch {0} Jitsi Raum/Räume erstellen.", "left": "Du kannst noch {0} Jitsi Raum/Räume erstellen.",
"moderationUrl": "Url für Moderation", "moderationUrl": "Url für Moderation",
"moderationStarts" : "Beginn Moderation", "moderationStarts": "Beginn Moderation",
"noQuota": "Deine Quota für Jitsi Räume ist leider aufgebraucht.", "noQuota": "Deine Quota für Jitsi Räume ist leider aufgebraucht.",
"notStarted" : "Die Konferenz hat noch nicht begonnen.", "notStarted": "Die Konferenz hat noch nicht begonnen.",
"room": "Name", "room": "Name",
"starts": "Beginn" "starts": "Beginn"
}, },
@ -60,12 +75,12 @@
".": "Via E-Mail teilen", ".": "Via E-Mail teilen",
"subject": "Einladung in Videokonferenz {0}" "subject": "Einladung in Videokonferenz {0}"
}, },
"text" : { "text": {
"both" : "Die Konferenz beginnt am {0} und endet am {1}.", "both": "Die Konferenz beginnt am {0} und endet am {1}.",
"expires" : "Die Konferenz endet am {0}.", "expires": "Die Konferenz endet am {0}.",
"intro" : "Hallo, ich möchte dich zu einer Videokonferenz einladen.", "intro": "Hallo, ich möchte dich zu einer Videokonferenz einladen.",
"outro" : "Du kannst der Konferenz unter folgendem Link beitreten:\n {0}", "outro": "Du kannst der Konferenz unter folgendem Link beitreten:\n {0}",
"starts" : "Die Konferenz beginnt am {0}." "starts": "Die Konferenz beginnt am {0}."
} }
} }
}, },
@ -286,6 +301,10 @@
"support": "Support", "support": "Support",
"text": "Zurzeit scheint der Dienst nicht erreichbar zu sein. Wenn diese Meldung länger besteht, melde dich beim Support!" "text": "Zurzeit scheint der Dienst nicht erreichbar zu sein. Wenn diese Meldung länger besteht, melde dich beim Support!"
}, },
"service": {
"name": "Name",
"text": "Beschreibung"
},
"services": { "services": {
".": "Dienste", ".": "Dienste",
"alias_creation": { "alias_creation": {
@ -302,6 +321,12 @@
"title": "Gitea" "title": "Gitea"
}, },
"goto": "Zum Dienst", "goto": "Zum Dienst",
"invite_partey": {
"icon": "cake",
"subtitle": "Einladung zur Partey",
"text": "Hier kannst du Einladungen für die Eröffnungs-Partey erstellen.",
"title": "Partey-Einladung"
},
"jitsi": { "jitsi": {
"icon": "video_call", "icon": "video_call",
"subtitle": "Video Konferenzen", "subtitle": "Video Konferenzen",
@ -320,6 +345,12 @@
"text": "mit anderen Austauschen, sich Informieren oder einfach quatschen.", "text": "mit anderen Austauschen, sich Informieren oder einfach quatschen.",
"title": "Matrix" "title": "Matrix"
}, },
"monitoring": {
"icon": "check",
"subtitle": "System Status",
"text": "Status und Perfomance der aktuellen Systeme",
"title": "Monitoring"
},
"nextcloud": { "nextcloud": {
"icon": "cloud", "icon": "cloud",
"subtitle": "Cloud Plattform", "subtitle": "Cloud Plattform",

View File

@ -27,6 +27,21 @@
"info": { "info": {
".": "Info" ".": "Info"
}, },
"invite": {
".": "Invite",
"create": "Create Invite",
"expires": "Expires",
"link": "Link",
"note": "Note",
"starts": "Starts"
},
"invites": {
".": "Invites",
"info": "You can create new invites here. To edit an invite like adding a note or change the personal invite message just click on the invite link. If you are authorized, you can change the texts directly on the invite page.",
"left": "You have {0} invites left.",
"others": "Other's invites",
"search": "Search"
},
"jitsi": { "jitsi": {
"rooms": { "rooms": {
".": "Jitsi Rooms", ".": "Jitsi Rooms",
@ -35,17 +50,17 @@
"delete": "Delete", "delete": "Delete",
"error": { "error": {
"expires": "Invalid expiry.", "expires": "Invalid expiry.",
"moderationStarts" : "Invalid moderation starts. Moderation have to start before conference starts", "moderationStarts": "Invalid moderation starts. Moderation have to start before conference starts",
"room": "Please choose a different name. The name is already taken or contains invalid characters. Only letters and numbers are allowed.", "room": "Please choose a different name. The name is already taken or contains invalid characters. Only letters and numbers are allowed.",
"starts": "Invalid start." "starts": "Invalid start."
}, },
"expires": "Expires", "expires": "Expires",
"info": "You can create new Jitsi Rooms here. The number is limited due to a quota.", "info": "You can create new Jitsi Rooms here. The number is limited due to a quota.",
"left": "You have {0} Jitsi Room(s) left.", "left": "You have {0} Jitsi Room(s) left.",
"moderationStarts" : "Moderation starts", "moderationStarts": "Moderation starts",
"moderationUrl": "Moderation url", "moderationUrl": "Moderation url",
"noQuota": "Your quota for Jitsi Rooms is depleted.", "noQuota": "Your quota for Jitsi Rooms is depleted.",
"notStarted" : "The conference has not started yet.", "notStarted": "The conference has not started yet.",
"room": "Name", "room": "Name",
"starts": "Starts" "starts": "Starts"
}, },
@ -273,6 +288,10 @@
"support": "Support", "support": "Support",
"text": "The service seems currently unavailable. If this message appears over a longer period, please contact our support!" "text": "The service seems currently unavailable. If this message appears over a longer period, please contact our support!"
}, },
"service": {
"name": "Name",
"text": "Description"
},
"services": { "services": {
".": "Services", ".": "Services",
"alias_creation": { "alias_creation": {
@ -289,6 +308,12 @@
"title": "Gitea" "title": "Gitea"
}, },
"goto": "To service", "goto": "To service",
"invite_partey": {
"icon": "cake",
"subtitle": "Invite to Partey",
"text": "Create Invites for Opening Partey.",
"title": "Partey-Invites"
},
"jitsi": { "jitsi": {
"icon": "video_call", "icon": "video_call",
"subtitle": "Video conferencing", "subtitle": "Video conferencing",
@ -307,6 +332,12 @@
"text": "talk, exchange, discuss with others", "text": "talk, exchange, discuss with others",
"title": "Matrix" "title": "Matrix"
}, },
"monitoring": {
"icon": "check",
"subtitle": "System Status",
"text": "Status and perfomance of current systems",
"title": "Monitoring"
},
"nextcloud": { "nextcloud": {
"icon": "cloud", "icon": "cloud",
"subtitle": "Cloud service", "subtitle": "Cloud service",