diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 7ff8d3e..ad34d04 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -19,6 +19,7 @@ import {VoucherComponent} from './pages/account/voucher/voucher.component'; import {SecurityComponent} from './pages/account/security/security.component'; import {UnavailableComponent} from './pages/unavailable/unavailable.component'; import {NotfoundComponent} from './pages/notfound/notfound.component'; +import {UserComponent} from './pages/user/user.component' const routes: Routes = [ {path: '', redirectTo: "/account/info", pathMatch: 'full'}, @@ -44,7 +45,8 @@ const routes: Routes = [ {path: 'register', component: RegisterComponent, canActivate: [AnonymousGuard]}, {path: 'tokens', component: TokensComponent, canActivate: [AuthGuard]}, {path: 'unavailable', component: UnavailableComponent}, - {path: '**', component: NotfoundComponent, pathMatch: 'full'}, + {path: 'p/:username', component: UserComponent, canActivate: [AuthUpdateGuard]}, + {path: '**', component: NotfoundComponent, pathMatch: 'full', canActivate: [AuthUpdateGuard]}, ]; @NgModule({ diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 5303ec4..09ca763 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -21,6 +21,11 @@ export class AppComponent { auth; constructor(private i18n: I18nService, private authService: AuthService, private router: Router, private iconRegistry: MatIconRegistry, private sanitizer: DomSanitizer, private _adapter: DateAdapter) { + iconRegistry.addSvgIcon('logo', sanitizer.bypassSecurityTrustResourceUrl('assets/icons/logo.svg')); + } + + ngOnInit() { + this.currentLocale = this.i18n.getLocale(); this.locales = this.i18n.getLocales(); this.authService.auth.subscribe(data => { @@ -29,10 +34,6 @@ export class AppComponent { 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; if(width < 768) { this.opened = false; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 23aaf8b..fe4156b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -33,13 +33,15 @@ import {UsernameDialog} from './pages/register/username-dialog/username.dialog'; import {UnavailableComponent} from './pages/unavailable/unavailable.component'; import {NotfoundComponent} from './pages/notfound/notfound.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'; export function init_app(i18n: I18nService) { - return () => i18n.fetch(i18n.getLocale()).then(response => {}, error => {}); + return () => i18n.fetch().then(response => {}, error => {}); } @Injectable() @@ -81,7 +83,9 @@ export class XhrInterceptor implements HttpInterceptor { UsernameDialog, UnavailableComponent, NotfoundComponent, - HtmlComponent + HtmlComponent, + ConfirmDialog, + UserComponent ], imports: [ BrowserModule, diff --git a/src/app/pages/user/user.component.html b/src/app/pages/user/user.component.html new file mode 100644 index 0000000..350f4c2 --- /dev/null +++ b/src/app/pages/user/user.component.html @@ -0,0 +1,19 @@ +
+ + +
+

{{username}}

+ +
+ + + + {{error.status}} + {{'user.unavailable' | i18n}} + + +

+ {{'user.unavailable.text' | i18n}} +

+
+
\ No newline at end of file diff --git a/src/app/pages/user/user.component.scss b/src/app/pages/user/user.component.scss new file mode 100644 index 0000000..728d157 --- /dev/null +++ b/src/app/pages/user/user.component.scss @@ -0,0 +1,3 @@ +h3 { + text-transform: uppercase; +} \ No newline at end of file diff --git a/src/app/pages/user/user.component.spec.ts b/src/app/pages/user/user.component.spec.ts new file mode 100644 index 0000000..e6bf596 --- /dev/null +++ b/src/app/pages/user/user.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserComponent } from './user.component'; + +describe('UserComponent', () => { + let component: UserComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ UserComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UserComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/user/user.component.ts b/src/app/pages/user/user.component.ts new file mode 100644 index 0000000..d04872e --- /dev/null +++ b/src/app/pages/user/user.component.ts @@ -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; + }) + } + + +} diff --git a/src/app/services/i18n.service.ts b/src/app/services/i18n.service.ts index c2194ac..5c48cf5 100644 --- a/src/app/services/i18n.service.ts +++ b/src/app/services/i18n.service.ts @@ -1,35 +1,18 @@ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { isEmpty } from 'rxjs/operators'; +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import { environment } from '../../environments/environment'; @Injectable({ providedIn: 'root', }) export class I18nService { - locale: String; - locales = ["de-informal"]; + locale: string; + locales : any = ["de-informal"]; i18n: any; 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() { @@ -44,25 +27,44 @@ export class I18nService { this.locale = locale; } - async fetch(locale) { - this.i18n = await this.http.get("./assets/i18n/" + locale + ".json").toPromise(); + async fetch() {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'; + } + + 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: any[]): String { + get(key, args: string[]): string { return this.getInternal(key, args, this.i18n); } - getInternal(key, args: any[], from): String { - if (!from) { + getInternal(key, args: string[], from): string { + if(!from) { return key; - } else if (from[key]) { - if (from[key]["."]) { + } else if(from[key]) { + if(from[key]["."]) { return this.insertArguments(from[key]["."], args); } return this.insertArguments(from[key], args); } else { let keys = key.split("."); - if (from[keys[0]]) { + if(from[keys[0]]) { key = keys.slice(1, keys.length).join("."); return this.getInternal(key, args, from[keys[0]]) } @@ -71,10 +73,10 @@ export class I18nService { return key; } - insertArguments(label: String, args: any[]) { - if (args) { - for (let index in args) { - label = label.replace(`{${index}}`, args[index]); + insertArguments(label: string, args: string[]) { + if(args) { + for(let index in args) { + label = label.replace(`{${index}}`, this.get(args[index], [])); } } return label; diff --git a/src/app/ui/confirm/confirm.component.html b/src/app/ui/confirm/confirm.component.html new file mode 100644 index 0000000..e2af757 --- /dev/null +++ b/src/app/ui/confirm/confirm.component.html @@ -0,0 +1,7 @@ + + {{text}} + + + + + \ No newline at end of file diff --git a/src/app/ui/confirm/confirm.component.scss b/src/app/ui/confirm/confirm.component.scss new file mode 100644 index 0000000..720774d --- /dev/null +++ b/src/app/ui/confirm/confirm.component.scss @@ -0,0 +1,3 @@ +mat-form-field { + display: block; +} \ No newline at end of file diff --git a/src/app/ui/confirm/confirm.component.ts b/src/app/ui/confirm/confirm.component.ts new file mode 100644 index 0000000..9bcb099 --- /dev/null +++ b/src/app/ui/confirm/confirm.component.ts @@ -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, + @Inject(MAT_DIALOG_DATA) public data: any) { + this.text = i18nService.get(data.label, data.args); + } + +} + diff --git a/src/app/ui/profilefields/profilefield.blob.html b/src/app/ui/profilefields/profilefield.blob.html index 9a5d98f..8e22cdd 100644 --- a/src/app/ui/profilefields/profilefield.blob.html +++ b/src/app/ui/profilefields/profilefield.blob.html @@ -1,4 +1,4 @@ -

+

{{'profileField.name.' + profileField.name | i18n}}

     {{profileField.blob}}
diff --git a/src/app/ui/profilefields/profilefield.dialog.html b/src/app/ui/profilefields/profilefield.dialog.html
index fe08ec0..96e4ce9 100644
--- a/src/app/ui/profilefields/profilefield.dialog.html
+++ b/src/app/ui/profilefields/profilefield.dialog.html
@@ -1,4 +1,4 @@
-

+

{{'profileField.name.' + profileField.name | i18n}}

diff --git a/src/app/ui/profilefields/profilefields.component.ts b/src/app/ui/profilefields/profilefields.component.ts index 9b55cc1..7df535d 100644 --- a/src/app/ui/profilefields/profilefields.component.ts +++ b/src/app/ui/profilefields/profilefields.component.ts @@ -4,6 +4,7 @@ import {FormBuilder, FormGroup, Validators} from '@angular/forms'; import {MatDialog, MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog'; import {I18nService} from '../../services/i18n.service'; import {ProfileService} from '../../services/profile.service'; +import {ConfirmDialog} from '../confirm/confirm.component'; @Component({ selector: 'app-profilefields', @@ -55,7 +56,7 @@ export class ProfileFieldsComponent implements OnInit { const dialogRef = this.dialog.open(ProfileFieldDialog, { data: profileField, - minWidth : '400px' + minWidth: '400px' }); @@ -74,16 +75,27 @@ export class ProfileFieldsComponent implements OnInit { } confirmDelete(profileField) { - this.profileService.delete(profileField.name).subscribe((result: any) => { - this.profileFields.splice(this.profileFields.indexOf(profileField), 1); - this.profileFields = [...this.profileFields]; + 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.profileFields.splice(this.profileFields.indexOf(profileField), 1); + this.profileFields = [...this.profileFields]; + }) + } + }); } openCreate() { const dialogRef = this.dialog.open(ProfileFieldDialog, { data: {"type": "TEXT", "visibility": "PRIVATE"}, - minWidth : '400px' + minWidth: '400px' }); dialogRef.afterClosed().subscribe(result => { @@ -100,7 +112,7 @@ export class ProfileFieldsComponent implements OnInit { openBlob(profileField) { this.dialog.open(ProfileFieldBlob, { data: profileField, - minWidth : '400px' + minWidth: '400px' }); } diff --git a/src/assets/i18n/de-informal.json b/src/assets/i18n/de-informal.json deleted file mode 100644 index 5951911..0000000 --- a/src/assets/i18n/de-informal.json +++ /dev/null @@ -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!" - } - } -} \ No newline at end of file diff --git a/src/assets/i18n/unset.json b/src/assets/i18n/unset.json deleted file mode 100644 index 9e26dfe..0000000 --- a/src/assets/i18n/unset.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file