profile + i18n

This commit is contained in:
Lurkars 2021-03-18 15:12:30 +01:00
parent 0b4fe16b8e
commit 3e26f43177
16 changed files with 184 additions and 349 deletions

View File

@ -19,6 +19,7 @@ import {VoucherComponent} from './pages/account/voucher/voucher.component';
import {SecurityComponent} from './pages/account/security/security.component'; import {SecurityComponent} from './pages/account/security/security.component';
import {UnavailableComponent} from './pages/unavailable/unavailable.component'; import {UnavailableComponent} from './pages/unavailable/unavailable.component';
import {NotfoundComponent} from './pages/notfound/notfound.component'; import {NotfoundComponent} from './pages/notfound/notfound.component';
import {UserComponent} from './pages/user/user.component'
const routes: Routes = [ const routes: Routes = [
{path: '', redirectTo: "/account/info", pathMatch: 'full'}, {path: '', redirectTo: "/account/info", pathMatch: 'full'},
@ -44,7 +45,8 @@ 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: 'unavailable', component: UnavailableComponent}, {path: 'unavailable', component: UnavailableComponent},
{path: '**', component: NotfoundComponent, pathMatch: 'full'}, {path: 'p/:username', component: UserComponent, canActivate: [AuthUpdateGuard]},
{path: '**', component: NotfoundComponent, pathMatch: 'full', canActivate: [AuthUpdateGuard]},
]; ];
@NgModule({ @NgModule({

View File

@ -21,6 +21,11 @@ export class AppComponent {
auth; auth;
constructor(private i18n: I18nService, private authService: AuthService, private router: Router, private iconRegistry: MatIconRegistry, private sanitizer: DomSanitizer, private _adapter: DateAdapter<any>) { constructor(private i18n: I18nService, private authService: AuthService, private router: Router, private iconRegistry: MatIconRegistry, private sanitizer: DomSanitizer, private _adapter: DateAdapter<any>) {
iconRegistry.addSvgIcon('logo', sanitizer.bypassSecurityTrustResourceUrl('assets/icons/logo.svg'));
}
ngOnInit() {
this.currentLocale = this.i18n.getLocale(); this.currentLocale = this.i18n.getLocale();
this.locales = this.i18n.getLocales(); this.locales = this.i18n.getLocales();
this.authService.auth.subscribe(data => { this.authService.auth.subscribe(data => {
@ -29,10 +34,6 @@ export class AppComponent {
this._adapter.setLocale(this.currentLocale); this._adapter.setLocale(this.currentLocale);
iconRegistry.addSvgIcon('logo', sanitizer.bypassSecurityTrustResourceUrl('assets/icons/logo.svg'));
}
ngOnInit() {
const width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; const width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
if(width < 768) { if(width < 768) {
this.opened = false; this.opened = false;

View File

@ -33,13 +33,15 @@ import {UsernameDialog} from './pages/register/username-dialog/username.dialog';
import {UnavailableComponent} from './pages/unavailable/unavailable.component'; import {UnavailableComponent} from './pages/unavailable/unavailable.component';
import {NotfoundComponent} from './pages/notfound/notfound.component'; import {NotfoundComponent} from './pages/notfound/notfound.component';
import {HtmlComponent} from './utils/html/html.component'; import {HtmlComponent} from './utils/html/html.component';
import {ConfirmDialog} from './ui/confirm/confirm.component'
import {UserComponent} from './pages/user/user.component'
import {I18nService} from './services/i18n.service'; import {I18nService} from './services/i18n.service';
export function init_app(i18n: I18nService) { export function init_app(i18n: I18nService) {
return () => i18n.fetch(i18n.getLocale()).then(response => {}, error => {}); return () => i18n.fetch().then(response => {}, error => {});
} }
@Injectable() @Injectable()
@ -81,7 +83,9 @@ export class XhrInterceptor implements HttpInterceptor {
UsernameDialog, UsernameDialog,
UnavailableComponent, UnavailableComponent,
NotfoundComponent, NotfoundComponent,
HtmlComponent HtmlComponent,
ConfirmDialog,
UserComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -0,0 +1,19 @@
<br>
<mat-progress-bar *ngIf="!success && !error" mode="indeterminate"></mat-progress-bar>
<div *ngIf="success">
<h3>{{username}}</h3>
<app-profilefields [profileFields]="profileFields"></app-profilefields>
</div>
<mat-card *ngIf="error">
<mat-card-header>
<mat-card-title>{{error.status}}</mat-card-title>
<mat-card-subtitle>{{'user.unavailable' | i18n}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p>
{{'user.unavailable.text' | i18n}}
</p>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,3 @@
h3 {
text-transform: uppercase;
}

View File

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

View File

@ -0,0 +1,35 @@
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute, Router, ParamMap} from '@angular/router';
import {ProfileService} from '../../services/profile.service';
@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.scss']
})
export class UserComponent implements OnInit {
username;
profileFields = [];
error = false;
success = false;
constructor(
private profileService: ProfileService,
private router: Router,
private route: ActivatedRoute) {}
async ngOnInit() {
this.username = this.route.snapshot.paramMap.get('username');
this.profileService.getAllForUser(this.username).subscribe((data: any) => {
this.profileFields = data;
this.success = true;
}, error => {
this.error = error;
})
}
}

View File

@ -1,35 +1,18 @@
import { Injectable } from '@angular/core'; import {Injectable} from '@angular/core';
import { HttpClient } from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import { isEmpty } from 'rxjs/operators'; import { environment } from '../../environments/environment';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class I18nService { export class I18nService {
locale: String; locale: string;
locales = ["de-informal"]; locales : any = ["de-informal"];
i18n: any; i18n: any;
constructor(private http: HttpClient) { constructor(private http: HttpClient) {
let browserLocale = navigator.language;
if (browserLocale.indexOf("-") != -1) {
browserLocale = browserLocale.split("-")[0];
}
let locale = localStorage.getItem("bstly.locale") || browserLocale || this.locales[0];
if (locale == 'de') {
locale = 'de-informal';
}
if (this.locales.indexOf(locale) == -1) {
locale = this.locales[0];
}
this.setLocale(locale);
} }
getLocales() { getLocales() {
@ -44,25 +27,44 @@ export class I18nService {
this.locale = locale; this.locale = locale;
} }
async fetch(locale) { async fetch() {let browserLocale = navigator.language;
this.i18n = await this.http.get("./assets/i18n/" + locale + ".json").toPromise();
if(browserLocale.indexOf("-") != -1) {
browserLocale = browserLocale.split("-")[0];
} }
get(key, args: any[]): String { let locale = localStorage.getItem("bstly.locale") || browserLocale || this.locales[0];
if(locale == 'de') {
locale = 'de-informal';
}
this.locales = await this.http.get(environment.apiUrl + "/i18n").toPromise();
if(this.locales.indexOf(locale) == -1) {
locale = this.locales[0];
}
this.i18n = await this.http.get(environment.apiUrl + "/i18n/" + locale).toPromise();
this.setLocale(locale);
}
get(key, args: string[]): string {
return this.getInternal(key, args, this.i18n); return this.getInternal(key, args, this.i18n);
} }
getInternal(key, args: any[], from): String { getInternal(key, args: string[], from): string {
if (!from) { if(!from) {
return key; return key;
} else if (from[key]) { } else if(from[key]) {
if (from[key]["."]) { if(from[key]["."]) {
return this.insertArguments(from[key]["."], args); return this.insertArguments(from[key]["."], args);
} }
return this.insertArguments(from[key], args); return this.insertArguments(from[key], args);
} else { } else {
let keys = key.split("."); let keys = key.split(".");
if (from[keys[0]]) { if(from[keys[0]]) {
key = keys.slice(1, keys.length).join("."); key = keys.slice(1, keys.length).join(".");
return this.getInternal(key, args, from[keys[0]]) return this.getInternal(key, args, from[keys[0]])
} }
@ -71,10 +73,10 @@ export class I18nService {
return key; return key;
} }
insertArguments(label: String, args: any[]) { insertArguments(label: string, args: string[]) {
if (args) { if(args) {
for (let index in args) { for(let index in args) {
label = label.replace(`{${index}}`, args[index]); label = label.replace(`{${index}}`, this.get(args[index], []));
} }
} }
return label; return label;

View File

@ -0,0 +1,7 @@
<mat-dialog-content>
{{text}}
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button [mat-dialog-close]="false">{{'cancel' | i18n}}</button>
<button mat-raised-button [mat-dialog-close]="true" color="accent">{{'confirm' | i18n}}</button>
</mat-dialog-actions>

View File

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

View File

@ -0,0 +1,21 @@
import {Component, Inject} from '@angular/core';
import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
import {I18nService} from '../../services/i18n.service';
@Component({
templateUrl: 'confirm.component.html',
styleUrls: ['./confirm.component.scss']
})
export class ConfirmDialog {
text;
constructor(private i18nService: I18nService,
public dialogRef: MatDialogRef<ConfirmDialog>,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.text = i18nService.get(data.label, data.args);
}
}

View File

@ -1,4 +1,4 @@
<h1 mat-dialog-title></h1> <h1 mat-dialog-title>{{'profileField.name.' + profileField.name | i18n}}</h1>
<mat-dialog-content> <mat-dialog-content>
<pre> <pre>
{{profileField.blob}} {{profileField.blob}}

View File

@ -1,4 +1,4 @@
<h1 mat-dialog-title></h1> <h1 mat-dialog-title>{{'profileField.name.' + profileField.name | i18n}}</h1>
<mat-dialog-content> <mat-dialog-content>
<form [formGroup]="form"> <form [formGroup]="form">
<mat-form-field> <mat-form-field>

View File

@ -4,6 +4,7 @@ import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {MatDialog, MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog'; import {MatDialog, MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
import {I18nService} from '../../services/i18n.service'; import {I18nService} from '../../services/i18n.service';
import {ProfileService} from '../../services/profile.service'; import {ProfileService} from '../../services/profile.service';
import {ConfirmDialog} from '../confirm/confirm.component';
@Component({ @Component({
selector: 'app-profilefields', selector: 'app-profilefields',
@ -55,7 +56,7 @@ export class ProfileFieldsComponent implements OnInit {
const dialogRef = this.dialog.open(ProfileFieldDialog, { const dialogRef = this.dialog.open(ProfileFieldDialog, {
data: profileField, data: profileField,
minWidth : '400px' minWidth: '400px'
}); });
@ -74,16 +75,27 @@ export class ProfileFieldsComponent implements OnInit {
} }
confirmDelete(profileField) { confirmDelete(profileField) {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'profileField.confirmDelete',
'args': ['profileField.name.' + profileField.name]
}
})
dialogRef.afterClosed().subscribe(result => {
if(result) {
this.profileService.delete(profileField.name).subscribe((result: any) => { this.profileService.delete(profileField.name).subscribe((result: any) => {
this.profileFields.splice(this.profileFields.indexOf(profileField), 1); this.profileFields.splice(this.profileFields.indexOf(profileField), 1);
this.profileFields = [...this.profileFields]; this.profileFields = [...this.profileFields];
}) })
} }
});
}
openCreate() { openCreate() {
const dialogRef = this.dialog.open(ProfileFieldDialog, { const dialogRef = this.dialog.open(ProfileFieldDialog, {
data: {"type": "TEXT", "visibility": "PRIVATE"}, data: {"type": "TEXT", "visibility": "PRIVATE"},
minWidth : '400px' minWidth: '400px'
}); });
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {
@ -100,7 +112,7 @@ export class ProfileFieldsComponent implements OnInit {
openBlob(profileField) { openBlob(profileField) {
this.dialog.open(ProfileFieldBlob, { this.dialog.open(ProfileFieldBlob, {
data: profileField, data: profileField,
minWidth : '400px' minWidth: '400px'
}); });
} }

View File

@ -1,298 +0,0 @@
{
"account": "Account",
"cancel": "Abbrechen",
"close": "Schliessen",
"date-time-format": "dd.MM.yyyy HH:mm:ss",
"email": {
".": "E-Mail Adresse",
"invalid": "ungültige E-Mail Adresse",
"primary": "primäre E-Mail Adresse"
},
"greet": "Hallo {0}",
"home": {
".": "Über we.bstly",
"club": {
".": "Verein",
"about": "Über den Verein",
"charter": "Satzung (Entwurf)",
"membership": "Mitgliedschaft"
},
"general": {
".": "Über we.bstly",
"we": "Was unser Ziel ist",
"what": "Was wir machen",
"you": "Was du machen kannst"
},
"privacy": {
".": "Datenschutz",
"design": "Privacy By Design",
"pretix": "Shop System (Pretix)",
"services": "Aktuelle Services",
"we-bstly": "we.bstly"
},
"services": {
".": "Services",
"active": "Aktive Services",
"email": "E-Mail Postfach",
"legend": {
".": "Legende",
"not-available": "⚠️ noch nicht konkret/technische Hürden",
"not-ready": "❔ noch nicht fertig",
"ready": "✅ fertig, benötigt nur Finanzierung"
},
"planned": "Geplante Services"
}
},
"homepage": "Homepage",
"i18n.test.replace": "Wat!?! {0} {1} {2}",
"imprint": "Impressum",
"info": {
".": "Info"
},
"locale": {
"de-informal": {
"long": "Deutsch",
"short": "DE"
},
"en": {
"long": "English",
"short": "EN"
}
},
"login": {
".": "Login",
"external": "Login",
"invalid": "Falscher Username oder Passwort.",
"keepSession": "Eingelogged bleiben"
},
"logout": "Logout",
"not-found": {
".": "Nicht gefunden",
"text": "Was geht ab!?"
},
"ok": "Ok",
"password": {
".": "Passwort",
"change": "Passwort ändern",
"changed": "Passwort erfolgreich geändert",
"confirm": "Passwort bestätigen",
"current": "Akutelles Passwort",
"error": {
"ILLEGAL_WHITESPACE": "Bitte keine Leerzeichen verwenden.",
"INSUFFICIENT_DIGIT": "Bitte mindestens eine Zahl eingeben.",
"INSUFFICIENT_LOWERCASE": "Bitte mindestens einen Kleinbuchstaben eingeben.",
"INSUFFICIENT_SPECIAL": "Bitte mindestens ein Sonderzeichen eingeben.",
"INSUFFICIENT_UPPERCASE": "Bitte mindestens einen Großbuchstaben eingeben.",
"TOO_SHORT": "Bitte ein längeres Passwort wählen."
},
"forgot": "Passwort vergessen",
"invalid": {
"hint": "Bitte gebe das Passwort in einem gültigen Format an."
},
"not-match": "Passwörter stimmen nicht überein.",
"request": "Neues Passwort anfordern",
"reset": {
".": "Passwort setzen",
"login": "Zum Login",
"success": {
"text": "Dein neues Passwort wurde übernommen. Du kannst dich nun mit deinem neuen Passwort einloggen.",
"title": "Passwort erfolgreich geändert"
}
}
},
"permissions": {
".": "Berechtigungen",
"expires": "Gültig bis",
"name": "Name",
"starts": "Gültig ab"
},
"pgp": {
".": "PGP",
"privateKey": {
".": "Privater PGP Schlüssel",
"confirmStore": "Ich habe meinen privaten Schlüssel sicher gespeichert!",
"downloadKey": "Privaten Schlüssel herunterladen"
}
},
"privacy-policy": "Datenschutzerklärung",
"profile": "Profil",
"profileField": {
".": "Profilfeld",
"create": "Neues Profilfeld hinzufügen",
"delete": "Löschen",
"edit": "Bearbeiten",
"index": "Index",
"name": {
".": "Name"
},
"openBlob": "Anzeigen",
"type": {
".": "Typ",
"BOOL": {
".": "Boolean"
},
"DATE": {
".": "Datum"
},
"EMAIL": {
".": "E-Mail"
},
"NUMBER": {
".": "Numerisch"
},
"TEXT": {
".": "Textfeld"
},
"URL": {
".": "URL"
}
},
"value": "Wert",
"visibility": {
".": "Sichtbarkeit",
"PRIVATE": {
".": "Privat"
},
"PROTECTED": {
".": "Geschützt"
},
"PUBLIC": {
".": "Öffentlich"
}
}
},
"quotas": {
".": "Quotas",
"name": "Name",
"unit": {
"#": "# (Anzahl)",
".": "Einheit",
"G": "GB (Gigabyte)"
},
"value": "Quota"
},
"register": {
".": "Registrierung",
"login": "Zum Login",
"success": {
"text": "Deine Registrierung war erfolgreich. Du kannst dich nun einloggen!",
"title": "Registrierung abgeschlossen"
},
"token.missing": "Du benötigst leider ein gültiges Token!"
},
"save": "Speichern",
"security": {
".": "Sicherheit",
"2fa": {
".": "Zwei-Faktor-Authentifierung (2FA)",
"info": "Du kannst hier einen zweiten Faktor zusätzlich zu deinem Passwort hinzufügen. Beachte, dass dies nur den Login in deinen we.bstly-Account betrifft. 2FA gilt nicht für deinen E-Mail Account. Aktuell wird nur TOTP (bekannt als Google Authenticator) unterstützt.",
"totp": {
".": "2FA (TOTP)",
"activate": "Um TOTP als 2FA zu aktivieren, gebe bitte deinen aktuellen Code ein.",
"code": "TOTP Code",
"create": "2FA (TOTP) einrichten",
"enable": "Aktiviere 2FA (TOTP)",
"external": "2FA (TOTP)",
"hint": "Um TOTP als zweiten Faktor beim Login zu verwenden, scanne den QRCode mit deiner TOTP App.",
"invalid": "TOTP Code ist ungültig",
"keepSession": "2FA (TOTP) für dieses Gerät merken",
"login": "Code verfizieren",
"missing": "Bitte TOTP Code eingeben",
"remove": "2FA (TOTP) deaktivieren"
}
}
},
"service-unavailable": {
".": "Service nicht erreichbar",
"text": "Zurzeit scheint der Service nicht erreichbar zu sein. Wenn diese Meldung länger besteht, melde dich bitte unter admin@bstly.de!"
},
"services": {
".": "Dienste",
"gitea": {
"icon": "code",
"subtitle": "Git-Repositories",
"text": "Alternative zu Diensten wie GitHub, Source Code von bstly-Entwicklungen",
"title": "Gitea"
},
"goto": "Zum Dienst",
"mail": {
"icon": "email",
"subtitle": "E-Mail Konto",
"text": "Catch-All an @{username}.we.bstly.de, lernender Spam-Filter und PGP Verschlüsselung.",
"title": "E-Mail Postfach"
},
"matrix": {
"icon": "question_answer",
"subtitle": "Messenger Plattform",
"text": "mit anderen Austauschen, sich Informieren oder einfach quatschen.",
"title": "Matrix"
},
"nextcloud": {
"icon": "cloud",
"subtitle": "Cloud Plattform",
"text": "Dateiverwaltung, Kalendar, Aufgabenmanagement, Kontaktmanagement, Abstimmungen und mehr.",
"title": "Nextcloud"
},
"partey": {
"icon": "celebration",
"subtitle": "Virtuelles Vereinsheim",
"text": "Digitaler Treffpunkt für Veranstaltungen oder einfach zum Abhängen.",
"title": "Partey"
},
"registration_vouchers": {
"icon": "card_giftcard",
"subtitle": "Gutschein Code für Registrierungs-Token",
"text": "Einladung um die Services des Bastelei e. V. zu nutzen",
"title": "Registrierungs-Gutscheincodes"
},
"ROLE_MEMBER": {
"icon": "loyalty",
"subtitle": "Mitgliedschaft im Bastelei e. V.",
"text": "Reguläres Mitglied im Bastelei e. V.",
"title": "Vereinsmitgliedschaft"
},
"wikijs": {
"icon": "school",
"subtitle": "Informationen, Dokumentation, Anleitungen",
"text": "Alle Information rund um Bastelei e. V. und den angebotenen Diensten, sowie Anleitungen für einzelne Dienste und Funktionen",
"title": "Wiki"
}
},
"software": "Software",
"sourcecode": "Quellcode",
"token": "Token",
"tokens": {
".": "Tokens",
"enter": "Token einlösen",
"get": "Mitgliedschaft",
"invalid": "Das Token ist leider nicht gültig.",
"provide-valid": "Bitte gebe ein gültiges Token ein.",
"redeem": "Tokens einlösen",
"redeemed": "Das Token wurde bereits eingelöst.",
"validate": "Prüfen"
},
"username": {
".": "Username",
"error": "Bitte wähle einen anderen Usernamen aus.",
"missing": "Bitte gebe einen Usernamen an."
},
"voucher": {
".": "Gutscheincode",
"code": "Code",
"type": "Typ"
},
"vouchers": {
".": "Gutscheincodes",
"add-on": "Add-On",
"info": "Hier kannst du Gutscheincodes für Add-Ons und Registrierung generieren.",
"registration": "Registrierung",
"stored-safely": {
".": "Da wir keine Verbindungen von Gutscheincodes zu deinem Account speichern, speichere diesen Code bitte selber sicher ab. Falls du die Seite verlässt oder neuläds ist der Code nicht mehr verfügbar!",
"confirm": "Ich habe den Code sicher abgespeicher!"
},
"temp": {
".": "Temporäre Gutscheincodes",
"info": "Hier werden deine aktuell angefragten Gutscheincodes angezeigt. Bitte speichere diese sicher ab, da wir diese Codes nicht für dich speichern!"
}
}
}

View File

@ -1 +0,0 @@
{}