initial commit

This commit is contained in:
2021-10-03 17:40:30 +02:00
commit 4db665a4c0
97 changed files with 19365 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
RewriteEngine On
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR]
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -d
RewriteCond %{REQUEST_URI} !^/api/.*$
RewriteRule ^ - [L]
RewriteRule ^ /index.html [L]
+32
View File
@@ -0,0 +1,32 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuard, AuthUpdateGuard, AuthenticatedGuard, AnonymousGuard } from './auth/auth.guard';
import { PageEntry } from './pages/entry/entry.page';
import { PageLogin } from './pages/login/login.page';
import { PageNew } from './pages/new/new.page';
import { PageNotFound} from './pages/notfound/notfound.page';
import { PageSettings } from './pages/settings/settings.page';
import { PageSubmission } from './pages/submission/submission.page';
import { PageTop } from './pages/top/top.page';
import { UiMain } from './ui/main/main.ui';
const routes: Routes = [
{
path: '', component: UiMain, children: [
{ path: '', component: PageTop, canActivate: [ AuthenticatedGuard ] },
{ path: 'e/:id', component: PageEntry, canActivate: [ AuthenticatedGuard ] },
{ path: 'new', component: PageNew, canActivate: [ AuthenticatedGuard ] },
{ path: 'submit', component: PageSubmission, canActivate: [ AuthenticatedGuard ] },
{ path: 'login', component: PageLogin, canActivate: [ AnonymousGuard ] },
{ path: 'settings', component: PageSettings, canActivate: [ AuthenticatedGuard ] },
]
},
];
@NgModule({
imports: [ RouterModule.forRoot(routes, { onSameUrlNavigation: 'reload', relativeLinkResolution: 'legacy' }) ],
exports: [ RouterModule ]
})
export class AppRoutingModule { }
+3
View File
@@ -0,0 +1,3 @@
<mat-drawer-container>
<router-outlet></router-outlet>
</mat-drawer-container>
View File
+35
View File
@@ -0,0 +1,35 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'bstlboard-angular'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('bstlboard-angular');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('bstlboard-angular app is running!');
});
});
+114
View File
@@ -0,0 +1,114 @@
import { Component, HostListener } from '@angular/core';
import { AuthService } from './services/auth.service';
import { I18nService } from './services/i18n.service';
import { Router } from '@angular/router';
import { DomSanitizer } from '@angular/platform-browser';
import { MatIconRegistry } from '@angular/material/icon';
import { DateAdapter } from '@angular/material/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: [ './app.component.scss' ]
})
export class AppComponent {
opened = true;
darkTheme = "false";
title = 'bstlboard';
currentLocale: String;
datetimeformat: String;
locales;
auth;
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.datetimeformat = this.i18n.get('format.datetime', []);
this.currentLocale = this.i18n.getLocale();
this.locales = this.i18n.getLocales();
this.authService.auth.subscribe(data => {
this.auth = data;
})
this._adapter.setLocale(this.currentLocale);
const width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
if (width < 768) {
this.opened = false;
} else {
this.opened = true;
}
if (localStorage.getItem("bstlboard.darkTheme") == "true") {
this.darkTheme = "true";
window.document.body.classList.add("dark-theme");
}
}
setLocale(locale) {
localStorage.setItem("bstlboard.locale", locale);
if (this.auth && this.auth.authenticated) {
window.location.reload();
} else {
window.location.reload();
}
}
darkThemeChange($event) {
if ($event.checked) {
this.darkTheme = "true";
} else {
this.darkTheme = "false";
}
localStorage.setItem("bstlboard.darkTheme", this.darkTheme);
if (this.auth && this.auth.authenticated) {
window.location.reload();
} else {
window.location.reload();
}
}
logout() {
this.authService.logout().subscribe(data => {
this.router.navigate([ "" ]).then(() => {
window.location.reload();
});
})
}
isBiggerScreen() {
const width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
if (width < 768) {
return false;
} else {
return true;
}
}
openExternal(url, target = '_self') {
window.open(url, target);
}
@HostListener('window:resize', [ '$event' ])
onResize(event) {
if (event.target.innerWidth < 768) {
this.opened = false;
} else {
this.opened = true;
}
}
}
+114
View File
@@ -0,0 +1,114 @@
import { NgModule, Injectable, APP_INITIALIZER } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HttpInterceptor, HttpHandler, HttpRequest, HTTP_INTERCEPTORS } from '@angular/common/http';
import { MaterialModule } from './material/material.module';
import { QRCodeModule } from 'angularx-qrcode';
import { DatePipe } from '@angular/common';
import { MatPaginatorIntl } from '@angular/material/paginator';
import { MAT_DATE_LOCALE } from '@angular/material/core';
import * as moment from 'moment';
import { AutofocusDirective } from './material/autofocus';
import { I18nPipe } from './utils/i18n.pipe';
import { MomentPipe } from './utils/moment.pipe';
import { AppComponent } from './app.component';
import { PageEntry } from './pages/entry/entry.page';
import { PageLogin } from './pages/login/login.page';
import { PageNew } from './pages/new/new.page';
import { PageNotFound } from './pages/notfound/notfound.page'
import { PageSettings } from './pages/settings/settings.page';
import { PageSubmission } from './pages/submission/submission.page';
import { PageTop } from './pages/top/top.page';
import { PageUnavailable } from './pages/unavailable/unavailable.page'
import { UiComment } from './ui/comment/comment.ui';
import { UiCommentCount } from './ui/commentcount/commentcount.ui';
import { UiCommentForm } from './ui/commentform/commentform.ui';
import { UiComments } from './ui/comments/comments.ui';
import { UiEntries } from './ui/entries/entries.ui';
import { UiEntry } from './ui/entry/entry.ui';
import { UiMain } from './ui/main/main.ui';
import { UiPoints } from './ui/points/points.ui';
import { ConfirmDialog } from './ui/confirm/confirm.component'
import { I18nService, I18nPaginatorIntl } from './services/i18n.service';
export function fetchI18n(i18n: I18nService) {
return () => i18n.fetch().then(response => { }, error => { });
}
export function setMaterialDate(i18n: I18nService) {
let locale = i18n.getLocale();
if (locale == 'de-informal') {
locale = 'de';
}
moment.locale(locale);
return locale;
}
@Injectable()
export class XhrInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler) {
const xhr = req.clone({
headers: req.headers.set('X-Requested-With', 'XMLHttpRequest').set('Content-Type', 'application/json;charset=UTF-8'), withCredentials: true
});
return next.handle(xhr);
}
}
@NgModule({
declarations: [
AutofocusDirective,
I18nPipe,
MomentPipe,
AppComponent,
PageEntry,
PageLogin,
PageNew,
PageNotFound,
PageSettings,
PageSubmission,
PageTop,
PageUnavailable,
UiComment,
UiCommentCount,
UiCommentForm,
UiComments,
UiEntries,
UiEntry,
UiMain,
UiPoints,
ConfirmDialog,
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
MaterialModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule,
QRCodeModule,
],
exports: [ MaterialModule ],
providers: [ { provide: APP_INITIALIZER, useFactory: fetchI18n, deps: [ I18nService ], multi: true }, { provide: MAT_DATE_LOCALE, useFactory: setMaterialDate, deps: [ I18nService ], multi: true }, { provide: HTTP_INTERCEPTORS, useClass: XhrInterceptor, multi: true }, DatePipe,
{
provide: MatPaginatorIntl, useFactory: (i18n) => {
const service = new I18nPaginatorIntl();
service.injectI18n(i18n)
return service;
}, deps: [ I18nService ]
} ],
bootstrap: [ AppComponent ],
})
export class AppModule {
}
+102
View File
@@ -0,0 +1,102 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../services/auth.service';
import { UserService } from '../services/user.service';
import { I18nService } from '../services/i18n.service';
@Injectable({
providedIn: 'root'
})
export class AuthUpdateGuard implements CanActivate {
constructor(private authService: AuthService) { }
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
this.authService.getAuth().catch(function (error) { });
return true;
}
}
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) { }
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const that = this;
return this.authService.getAuth().then(response => {
return true;
}).catch(function (error) {
return that.router.navigateByUrl(that.router.parseUrl('/unavailable?target=' + next.url), { skipLocationChange: true });
});
}
}
@Injectable({
providedIn: 'root'
})
export class AuthenticatedGuard implements CanActivate {
constructor(private authService: AuthService, private userService: UserService, private i18nService: I18nService, private router: Router) { }
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const that = this;
return this.authService.getAuth().then((data: any) => {
if (!data || !data.authenticated) {
return that.router.navigateByUrl(that.router.parseUrl('/login?target=' + encodeURIComponent(state.url)), { skipLocationChange: true, replaceUrl: true });
}
this.userService.get().subscribe((user: any) => {
let updateLocale = false;
let updateTheme = false;
let darktheme = 'false';
if (user.darkTheme) {
darktheme = 'true';
}
if (darktheme != localStorage.getItem("bstlboard.darkTheme")) {
localStorage.setItem("bstlboard.darkTheme", darktheme);
updateTheme = true;
}
if (this.i18nService.locales.indexOf(user.locale) != -1 && localStorage.getItem("bstlboard.locale") != user.locale) {
if (this.i18nService.locale != user.locale) {
localStorage.setItem("bstlboard.locale", user.locale);
updateLocale = true;
}
}
if (updateLocale || updateTheme) {
window.location.reload();
}
});
return true;
}).catch(function (error) {
return that.router.navigateByUrl(that.router.parseUrl('/unavailable?target=' + next.url), { skipLocationChange: true });
});
}
}
@Injectable({
providedIn: 'root'
})
export class AnonymousGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) { }
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const that = this;
return this.authService.getAuth().then((data: any) => {
if (data && data.authenticated) {
this.router.navigateByUrl('/');
return false;
}
return true;
}).catch(function (error) {
return that.router.navigateByUrl(that.router.parseUrl('/unavailable?target=' + next.url), { replaceUrl: true });
});
}
}
+18
View File
@@ -0,0 +1,18 @@
import { Directive, ElementRef, OnInit } from '@angular/core';
import { MatInput } from '@angular/material/input';
@Directive({
selector: '[matAutofocus]',
})
export class AutofocusDirective implements OnInit {
constructor(private element: ElementRef) { }
ngOnInit() {
setTimeout(() => {
this.element.nativeElement.focus();
this.element.nativeElement.scrollIntoView();
})
}
}
+141
View File
@@ -0,0 +1,141 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
// Material Form Controls
import {MatAutocompleteModule} from '@angular/material/autocomplete';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {MatDatepickerModule} from '@angular/material/datepicker';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import {MatRadioModule} from '@angular/material/radio';
import {MatSelectModule} from '@angular/material/select';
import {MatSliderModule} from '@angular/material/slider';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
// Material Navigation
import {MatMenuModule} from '@angular/material/menu';
import {MatSidenavModule} from '@angular/material/sidenav';
import {MatToolbarModule} from '@angular/material/toolbar';
// Material Layout
import {MatCardModule} from '@angular/material/card';
import {MatDividerModule} from '@angular/material/divider';
import {MatExpansionModule} from '@angular/material/expansion';
import {MatGridListModule} from '@angular/material/grid-list';
import {MatListModule} from '@angular/material/list';
import {MatStepperModule} from '@angular/material/stepper';
import {MatTabsModule} from '@angular/material/tabs';
import {MatTreeModule} from '@angular/material/tree';
// Material Buttons & Indicators
import {MatButtonModule} from '@angular/material/button';
import {MatButtonToggleModule} from '@angular/material/button-toggle';
import {MatBadgeModule} from '@angular/material/badge';
import {MatChipsModule} from '@angular/material/chips';
import {MatIconModule} from '@angular/material/icon';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {MatProgressBarModule} from '@angular/material/progress-bar';
import {MatRippleModule} from '@angular/material/core';
// Material Popups & Modals
import {MatBottomSheetModule} from '@angular/material/bottom-sheet';
import {MatDialogModule} from '@angular/material/dialog';
import {MatSnackBarModule} from '@angular/material/snack-bar';
import {MatTooltipModule} from '@angular/material/tooltip';
// Material Data tables
import {MatPaginatorModule} from '@angular/material/paginator';
import {MatSortModule} from '@angular/material/sort';
import {MatTableModule} from '@angular/material/table';
import {MatMomentDateModule} from '@angular/material-moment-adapter';
import {FlexLayoutModule} from '@angular/flex-layout';
import {
NgxMatDatetimePickerModule,
NgxMatNativeDateModule,
NgxMatTimepickerModule
} from '@angular-material-components/datetime-picker';
@NgModule({
declarations: [],
imports: [
CommonModule,
MatAutocompleteModule,
MatCheckboxModule,
MatDatepickerModule,
MatFormFieldModule,
MatInputModule,
MatRadioModule,
MatSelectModule,
MatSliderModule,
MatSlideToggleModule,
MatMenuModule,
MatSidenavModule,
MatToolbarModule,
MatCardModule,
MatDividerModule,
MatExpansionModule,
MatGridListModule,
MatListModule,
MatStepperModule,
MatTabsModule,
MatTreeModule,
MatButtonModule,
MatButtonToggleModule,
MatBadgeModule,
MatChipsModule,
MatIconModule,
MatProgressSpinnerModule,
MatProgressBarModule,
MatRippleModule,
MatBottomSheetModule,
MatDialogModule,
MatSnackBarModule,
MatTooltipModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
MatMomentDateModule,
FlexLayoutModule,
NgxMatDatetimePickerModule,
NgxMatNativeDateModule,
NgxMatTimepickerModule
],
exports: [
MatAutocompleteModule,
MatCheckboxModule,
MatDatepickerModule,
MatFormFieldModule,
MatInputModule,
MatRadioModule,
MatSelectModule,
MatSliderModule,
MatSlideToggleModule,
MatMenuModule,
MatSidenavModule,
MatToolbarModule,
MatCardModule,
MatDividerModule,
MatExpansionModule,
MatGridListModule,
MatListModule,
MatStepperModule,
MatTabsModule,
MatTreeModule,
MatButtonModule,
MatButtonToggleModule,
MatBadgeModule,
MatChipsModule,
MatIconModule,
MatProgressSpinnerModule,
MatProgressBarModule,
MatRippleModule,
MatBottomSheetModule,
MatDialogModule,
MatSnackBarModule,
MatTooltipModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
FlexLayoutModule,
NgxMatDatetimePickerModule,
NgxMatNativeDateModule,
NgxMatTimepickerModule
]
})
export class MaterialModule {}
+10
View File
@@ -0,0 +1,10 @@
<page-notfound *ngIf="notfound"></page-notfound>
<ng-container *ngIf="entry">
<ui-entry [entry]="entry" [change]="boundRefresh"></ui-entry>
<p>{{entry.text}}</p>
<ui-commentform [target]="entry.id" [change]="boundRefresh"></ui-commentform>
<ui-comments [target]="entry.id"></ui-comments>
</ng-container>
+37
View File
@@ -0,0 +1,37 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { EntriesService } from '../../services/entries.service';
@Component({
selector: 'page-entry',
templateUrl: './entry.page.html'
})
export class PageEntry implements OnInit {
id: number;
entry: any;
notfound: boolean = false;
boundRefresh: Function;
constructor(private entriesService: EntriesService,
private route: ActivatedRoute) { }
ngOnInit(): void {
this.id = +this.route.snapshot.paramMap.get('id');
this.boundRefresh = this.refresh.bind(this);
this.refresh();
}
refresh() {
this.entriesService.getEntry(this.id).subscribe((data) => {
this.entry = data;
}, (error) => {
if (error.status == 404) {
this.notfound = true;
}
})
}
}
+45
View File
@@ -0,0 +1,45 @@
<mat-card *ngIf="externals && externals.length > 0">
<mat-card-content>
<h2>{{'login.external' | i18n}}</h2>
<mat-error *ngIf="externalLoginInvalid">
{{'login.external.invalid' | i18n}}
</mat-error>
</mat-card-content>
<mat-card-actions>
<a class="external-login" href="{{apiUrl}}/{{client.loginUrl}}" *ngFor="let client of externals"
mat-raised-button color="accent">{{'login.external.client' | i18n:client.id}}</a>
</mat-card-actions>
</mat-card>
<form *ngIf="internalLogin || externals && externals.length < 1" action="{{apiUrl}}/login" method="POST" #loginForm>
<mat-card>
<mat-card-content>
<h2>{{'login.internal' | i18n}}</h2>
<mat-error *ngIf="loginInvalid">
{{'login.invalid' | i18n}}
</mat-error>
<mat-form-field>
<input id="username" name="username" matInput placeholder="{{'username' | i18n}}" required matAutofocus>
<mat-error>
{{'username.missing' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input id="password" name="password" matInput type="password" placeholder="{{'password' | i18n}}"
required>
<mat-error>
{{'password.invalid.hint' | i18n}}
</mat-error>
</mat-form-field>
<mat-slide-toggle id="remember-me" name="remember-me">
{{'login.keepSession' | i18n}}
</mat-slide-toggle>
</mat-card-content>
<mat-card-actions>
<button type="submit" (click)="loginForm.submit()" mat-raised-button color="primary"
[disabled]="loginForm.invalid">{{'login' |
i18n}}<mat-icon style="font-size: 1em;">open_in_new
</mat-icon></button>
</mat-card-actions>
</mat-card>
</form>
+8
View File
@@ -0,0 +1,8 @@
mat-form-field {
display: block;
}
a.external-login {
margin: 15px 0;
display: block;
}
+60
View File
@@ -0,0 +1,60 @@
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { environment } from '../../../environments/environment';
import { AuthService } from '../../services/auth.service';
@Component({
selector: 'page-login',
templateUrl: './login.page.html',
styleUrls: [ './login.page.scss' ]
})
export class PageLogin implements OnInit {
@ViewChild('loginForm') loginForm: ElementRef;
internalLogin: boolean;
loginInvalid: boolean;
externalLoginInvalid: boolean;
apiUrl = environment.apiUrl;
targetRoute: string;
externals: any[];
constructor(
private authService: AuthService,
private router: Router,
private route: ActivatedRoute) { }
async ngOnInit() {
this.route.queryParams.subscribe(params => {
if (params[ 'all' ] || params[ 'all' ] == '') {
this.internalLogin = true;
}
if (params[ 'target' ]) {
this.targetRoute = params[ 'target' ];
this.router.navigate([], { queryParams: { target: null }, queryParamsHandling: 'merge', replaceUrl: true });
}
if (params[ 'error' ] || params[ 'error' ] == '') {
this.loginInvalid = true;
this.router.navigate([], { queryParams: { error: null }, queryParamsHandling: 'merge', replaceUrl: true });
}
if (params[ 'externalError' ] || params[ 'externalError' ] == '') {
this.externalLoginInvalid = true;
this.router.navigate([], { queryParams: { externalError: null }, queryParamsHandling: 'merge', replaceUrl: true });
}
});
this.authService.getExternal().subscribe((data: any[]) => {
this.externals = data;
})
}
ngAfterViewInit(): void {
if (this.targetRoute) {
this.loginForm.nativeElement.action = this.loginForm.nativeElement.action + "?forward=" + window.location.origin + encodeURIComponent(this.targetRoute);
}
}
}
+1
View File
@@ -0,0 +1 @@
<ui-entries [entries]="entries" [refresh]="boundRefresh" [update]="boundUpdate"></ui-entries>
+43
View File
@@ -0,0 +1,43 @@
import { Component, OnInit } from '@angular/core';
import { EntriesService } from '../../services/entries.service';
import { PageEvent } from '@angular/material/paginator';
@Component({
selector: 'page-new',
templateUrl: './new.page.html'
})
export class PageNew implements OnInit {
entries: any;
boundRefresh: Function;
boundUpdate: Function;
constructor(private entriesService: EntriesService) { }
ngOnInit(): void {
this.refresh();
this.boundRefresh = this.refresh.bind(this);
this.boundUpdate = this.update.bind(this);
}
refresh(): void {
if (!this.entries) {
this.entriesService.getNew().subscribe((data) => {
this.entries = data;
})
} else {
this.entries.content = null;
this.entriesService.getNewPages(this.entries.number || 0, this.entries.size || 10).subscribe((data: any) => {
this.entries = data;
}, (error) => { })
}
}
update(event: PageEvent) {
this.entries.content = null;
this.entriesService.getNewPages(event.pageIndex, event.pageSize).subscribe((data: any) => {
this.entries = data;
}, (error) => { })
}
}
+11
View File
@@ -0,0 +1,11 @@
<mat-card class="accent">
<mat-card-header>
<mat-card-title>404</mat-card-title>
<mat-card-subtitle>{{'not-found' | i18n}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p>
{{'not-found.text' | i18n}}
</p>
</mat-card-content>
</mat-card>
+14
View File
@@ -0,0 +1,14 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'page-notfound',
templateUrl: './notfound.page.html'
})
export class PageNotFound implements OnInit {
constructor() { }
ngOnInit(): void {
}
}
+30
View File
@@ -0,0 +1,30 @@
<form [formGroup]="form" (ngSubmit)="save()" #formDirective="ngForm" *ngIf="user">
<mat-card>
<mat-card-content>
<mat-form-field>
<input matInput formControlName="username" type="text">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="{{'settings.email' | i18n}}" formControlName="email" type="email">
<mat-error *ngIf="hasError('email')">
{{'settings.email.error' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<textarea matInput placeholder="{{'settings.about' | i18n}}" formControlName="about"></textarea>
<mat-error>
{{'settings.about.error' | i18n}}
</mat-error>
</mat-form-field>
<mat-slide-toggle (change)="darkThemeChange($event)" [checked]="user.darkTheme">
{{'settings.darkTheme' | i18n}}
</mat-slide-toggle>
</mat-card-content>
<mat-card-actions>
<button *ngIf="!working" mat-raised-button color="primary" [disabled]="form.invalid">
{{'settings.update' | i18n}}
</button>
</mat-card-actions>
</mat-card>
</form>
@@ -0,0 +1,3 @@
mat-form-field {
display: block;
}
+80
View File
@@ -0,0 +1,80 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, Validators, NgForm } from '@angular/forms';
import { UserService } from '../../services/user.service';
@Component({
selector: 'page-settings',
templateUrl: './settings.page.html',
styleUrls: [ './settings.page.scss' ]
})
export class PageSettings implements OnInit {
auth: any;
user: any;
working: boolean = false;
form: FormGroup;
@ViewChild('formDirective') private formDirective: NgForm;
constructor(
private userService: UserService,
private formBuilder: FormBuilder) { }
ngOnInit(): void {
this.form = this.formBuilder.group({
username: [ { disabled: true }, Validators.nullValidator ],
email: [ '', Validators.nullValidator ],
about: [ '', Validators.nullValidator ],
darkTheme: [ '', Validators.nullValidator ],
});
this.form.get('username').disable();
this.userService.get().subscribe(user => {
this.user = user;
this.form.get('username').setValue(this.user.username);
this.form.get('email').setValue(this.user.email);
this.form.get('about').setValue(this.user.about);
this.form.get('darkTheme').setValue(this.user.darkTheme);
})
}
darkThemeChange($event) {
this.user.darkTheme = $event.checked;
}
hasError(controlName: string): boolean {
return this.form.controls[ controlName ].errors != null;
}
save(): void {
if (this.working) {
return;
}
this.working = true;
this.user.about = this.form.get('about').value;
this.user.email = this.form.get('email').value;
this.userService.update(this.user).subscribe((data) => {
this.user = data;
this.working = false;
}, (error) => {
this.working = false;
if (error.status == 422) {
let errors = {};
for (let code of error.error) {
errors[ code.field ] = errors[ code.field ] || {};
errors[ code.field ][ code.code ] = true;
}
for (let code in errors) {
this.form.get(code).setErrors(errors[ code ]);
}
}
})
}
}
@@ -0,0 +1,44 @@
<form [formGroup]="form" (ngSubmit)="create()" #formDirective="ngForm">
<mat-card>
<mat-card-content>
<p>{{'submission.info' | i18n}}</p>
<mat-form-field>
<mat-select placeholder="{{'submission.entryType' | i18n}}" formControlName="entryType">
<mat-select-trigger>
<mat-icon>{{'entryType.' + entryType + '.icon' | i18n}}</mat-icon> {{'entryType.' + entryType | i18n}}
</mat-select-trigger>
<mat-option *ngFor="let entryType of entryTypes" [value]="entryType" >
<mat-icon>{{'entryType.' + entryType + '.icon' | i18n}}</mat-icon> {{'entryType.' + entryType | i18n}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<input matInput placeholder="{{'submission.url' | i18n}}" formControlName="url" type="url" [required]="entryType == 'LINK'" matAutofocus>
<mat-error *ngIf="hasError('url')">
{{'submission.url.error' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput placeholder="{{'submission.title' | i18n}}" formControlName="title" type="text" required>
<mat-error>
{{'submission.title.error' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<textarea matInput placeholder="{{'submission.text' | i18n}}" formControlName="text"></textarea>
<mat-error>
{{'submission.text.error' | i18n}}
</mat-error>
</mat-form-field>
</mat-card-content>
<mat-card-actions>
<button *ngIf="!working" mat-raised-button color="primary" [disabled]="form.invalid">
{{'submission.create' | i18n}}
</button>
</mat-card-actions>
</mat-card>
</form>
@@ -0,0 +1,3 @@
mat-form-field {
display: block;
}
@@ -0,0 +1,80 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { EntriesService } from '../../services/entries.service';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators, NgForm } from '@angular/forms';
@Component({
selector: 'page-submission',
templateUrl: './submission.page.html',
styleUrls: [ './submission.page.scss' ]
})
export class PageSubmission implements OnInit {
entryTypes: string[] = [ 'LINK', 'DISCUSSION', 'QUESTION', 'INTERN' ];
entryType: string = this.entryTypes[ 0 ];
working: boolean = false;
form: FormGroup;
@ViewChild('formDirective') private formDirective: NgForm;
constructor(private entriesService: EntriesService,
private router: Router,
private formBuilder: FormBuilder) { }
ngOnInit(): void {
this.form = this.formBuilder.group({
entryType: [ '', Validators.required ],
url: [ '', Validators.required ],
title: [ '', Validators.required ],
text: [ '', Validators.nullValidator ],
});
this.form.get('entryType').setValue(this.entryType);
this.form.get('entryType').valueChanges.subscribe((value) => {
this.entryType = value;
if (value == 'LINK') {
this.form.get('url').setValidators([ Validators.required ]);
} else {
this.form.get('url').setValidators([ Validators.nullValidator ]);
}
});
}
hasError(controlName: string): boolean {
return this.form.controls[ controlName ].errors != null;
}
create(): void {
if (this.working) {
return;
}
this.working = true;
const entry: any = {};
entry.url = this.form.get("url").value;
entry.entryType = this.entryType;
entry.title = this.form.get("title").value;
entry.text = this.form.get("text").value;
this.entriesService.create(entry).subscribe((data) => {
this.router.navigateByUrl('/');
}, (error) => {
this.working = false;
if (error.status == 422) {
let errors = {};
for (let code of error.error) {
errors[ code.field ] = errors[ code.field ] || {};
errors[ code.field ][ code.code ] = true;
}
for (let code in errors) {
this.form.get(code).setErrors(errors[ code ]);
}
}
})
}
}
+1
View File
@@ -0,0 +1 @@
<ui-entries [entries]="entries" [refresh]="boundRefresh" [update]="boundUpdate"></ui-entries>
+82
View File
@@ -0,0 +1,82 @@
import { Component, OnInit, Input } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { EntriesService } from '../../services/entries.service';
import { PageEvent } from '@angular/material/paginator';
@Component({
selector: 'page-top',
templateUrl: './top.page.html'
})
export class PageTop implements OnInit {
@Input() entries: any;
boundRefresh: Function;
boundUpdate: Function;
init: boolean = true;
constructor(private entriesService: EntriesService, private router: Router, private route: ActivatedRoute) { }
ngOnInit(): void {
this.boundRefresh = this.refresh.bind(this);
this.boundUpdate = this.update.bind(this);
this.route.queryParams.subscribe(params => {
if (this.init) {
this.entries = {};
if (params[ 'p' ]) {
this.entries.number = +params[ 'p' ] - 1;
if (this.entries.number < 0) {
this.entries.number = 0;
}
}
if (params[ 's' ]) {
this.entries.size = +params[ 's' ];
}
this.refresh();
this.init = false;
}
});
}
refresh(): void {
if (!this.entries) {
this.entries = {};
}
this.entries.content = null;
this.entriesService.getPages(this.entries.number || 0, this.entries.size || 30).subscribe((data: any) => {
this.entries = data;
}, (error) => { })
}
update(event: PageEvent) {
this.entries.content = null;
const params: any = { p: null, s: null };
if (event.pageIndex != 0) {
params.p = event.pageIndex + 1;
}
if (event.pageSize != 30) {
params.s = event.pageSize;
}
this.router.navigate(
[],
{
relativeTo: this.route,
queryParams: params,
queryParamsHandling: 'merge'
});
this.entriesService.getPages(event.pageIndex, event.pageSize).subscribe((data: any) => {
this.entries = data;
}, (error) => { })
}
}
@@ -0,0 +1,19 @@
<mat-card class="warn">
<mat-card-header>
<mat-card-title>503</mat-card-title>
<mat-card-subtitle>{{'service-unavailable' | i18n}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p>
{{'service-unavailable.text' | i18n}}
</p>
</mat-card-content>
<mat-card-actions>
<a mat-raised-button color="primary" (click)="retry()">
{{'service-unavailable.retry' | i18n}}
</a>
<a mat-raised-button href="https://wiki.bstly.de/help#Support">
{{'service-unavailable.support' | i18n}}
</a>
</mat-card-actions>
</mat-card>
@@ -0,0 +1,36 @@
import { Component, OnInit } from '@angular/core';
import { Location } from '@angular/common'
import { Router, ActivatedRoute } from '@angular/router';
@Component({
selector: 'page-unavailable',
templateUrl: './unavailable.page.html'
})
export class PageUnavailable implements OnInit {
targetRoute = '';
constructor(
private location: Location,
private router: Router,
private route: ActivatedRoute) { }
ngOnInit(): void {
this.route.queryParams.subscribe(params => {
if (params[ 'target' ]) {
this.targetRoute = params[ 'target' ];
this.router.navigate([], { queryParams: { target: null }, queryParamsHandling: 'merge', skipLocationChange: true });
}
});
}
retry() {
if (!this.targetRoute || this.targetRoute === "unavailable" || this.targetRoute === "/unavailable") {
this.location.back;
} else {
this.router.navigate([ this.targetRoute ], { skipLocationChange: true });
}
}
}
+38
View File
@@ -0,0 +1,38 @@
import { Injectable } from '@angular/core';
import { ReplaySubject, of } from 'rxjs';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { environment } from './../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class AuthService {
auth: ReplaySubject<any> = new ReplaySubject(undefined);
constructor(private http: HttpClient) {
}
getAuth() {
return this.authMe().toPromise().then((data: any) => {
this.auth.next(data);
return data;
}, error => {
throw new Error(error);
});
}
authMe() {
return this.http.get(environment.apiUrl + "/auth");
}
getExternal() {
return this.http.get(environment.apiUrl + "/auth/external");
}
logout() {
return this.http.post(environment.apiUrl + "/logout", {});
}
}
+62
View File
@@ -0,0 +1,62 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class CommentService {
constructor(private http: HttpClient) {
}
get(target: number) {
return this.http.get(environment.apiUrl + "/c/e/" + target);
}
getParent(target: number, parent: number) {
return this.http.get(environment.apiUrl + "/c/e/" + target + "/" + parent);
}
count(target: number) {
return this.http.get(environment.apiUrl + "/c/c/" + target);
}
countParent(target: number, parent: number) {
return this.http.get(environment.apiUrl + "/c/c/" + target + "/" + parent);
}
getPages(target: number, page: number, size: number) {
return this.http.get(environment.apiUrl + "/c/e/" + target + "?page=" + page + "&size=" + size);
}
getParentPages(target: number, parent: number, page: number, size: number) {
return this.http.get(environment.apiUrl + "/c/e/" + target + "/" + parent + "?page=" + page + "&size=" + size);
}
getNew(target: number) {
return this.http.get(environment.apiUrl + "/c/e/new/" + target);
}
getNewParent(target: number, parent: number) {
return this.http.get(environment.apiUrl + "/c/e/new/" + target + "/" + parent);
}
getNewPages(target: number, page: number, size: number) {
return this.http.get(environment.apiUrl + "/c/e/new/" + target + "?page=" + page + "&size=" + size);
}
getNewParentPages(target: number, parent: number, page: number, size: number) {
return this.http.get(environment.apiUrl + "/c/e/new/" + target + "/" + parent + "?page=" + page + "&size=" + size);
}
getComment(id: number) {
return this.http.get(environment.apiUrl + "/c/" + id);
}
create(comment: any) {
comment.type = 'COMMENT';
return this.http.post(environment.apiUrl + "/c", comment);
}
}
+38
View File
@@ -0,0 +1,38 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class EntriesService {
constructor(private http: HttpClient) {
}
get() {
return this.http.get(environment.apiUrl + "/e");
}
getPages(page: number, size: number) {
return this.http.get(environment.apiUrl + "/e?page=" + page + "&size=" + size);
}
getNew() {
return this.http.get(environment.apiUrl + "/e/new");
}
getNewPages(page: number, size: number) {
return this.http.get(environment.apiUrl + "/e/new?page=" + page + "&size=" + size);
}
getEntry(id: number) {
return this.http.get(environment.apiUrl + "/e/" + id);
}
create(entry: any) {
entry.type = 'ENTRY';
return this.http.post(environment.apiUrl + "/e", entry);
}
}
+150
View File
@@ -0,0 +1,150 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';
import { MatPaginatorIntl } from '@angular/material/paginator';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class I18nService {
locale: string = "en";
locales: any[] = [ "en" ];
i18n: any;
constructor(private http: HttpClient) {
}
getLocales() {
return this.locales;
}
getLocale() {
return this.locale || 'en';
}
setLocale(locale) {
this.locale = locale;
}
async fetch() {
let browserLocale = navigator.language;
if (browserLocale.indexOf("-") != -1) {
browserLocale = browserLocale.split("-")[ 0 ];
}
let locale = localStorage.getItem("bstlboard.locale") || browserLocale || this.locales[ 0 ];
if (locale == 'de') {
locale = 'de-informal';
}
try {
await this.http.get(environment.apiUrl + "/i18n").toPromise().then((response: any) => {
this.locales = response;
});
} catch (e) {
console.debug("fallback to default locales");
}
if (this.locales.indexOf(locale) == -1) {
locale = this.locales[ 0 ];
}
this.setLocale(locale);
try {
this.i18n = await this.http.get(environment.apiUrl + "/i18n/" + locale).toPromise();
} catch (e) {
this.i18n = await this.http.get("/assets/i18n/" + locale + ".json").toPromise();
console.debug("fallback to default locale");
}
}
get(key, args: string[]): string {
return this.getInternal(key, args, this.i18n);
}
getInternal(key, args: string[], from): string {
key += '';
if (!from) {
if (args && args.length > 0) {
return key + "[" + args + "]";
}
return key;
} else if (from[ key ]) {
if (typeof from[ key ] === 'object') {
if (from[ key ][ "." ]) {
return this.insertArguments(from[ key ][ "." ], args);
}
if (args && args.length > 0) {
return key + "[" + args + "]";
}
}
return this.insertArguments(from[ key ], args);
} else {
let keys = key.split(".");
if (from[ keys[ 0 ] ]) {
key = keys.slice(1, keys.length).join(".");
return this.getInternal(key, args, from[ keys[ 0 ] ])
}
}
if (args && args.length > 0) {
return key + "[" + args + "]";
}
return key;
}
insertArguments(label: string, args: string[]) {
if (args) {
for (let index in args) {
label = label.replace(`{${index}}`, this.get(args[ index ], []));
}
}
return label;
}
}
@Injectable()
export class I18nPaginatorIntl implements MatPaginatorIntl {
changes = new Subject<void>();
i18n: I18nService;
itemsPerPageLabel: string;
nextPageLabel: string;
previousPageLabel: string;
firstPageLabel: string;
lastPageLabel: string;
injectI18n(i18n: I18nService) {
this.i18n = i18n;
this.firstPageLabel = this.i18n.get('paginator.firstPage', []);
this.itemsPerPageLabel = this.i18n.get('paginator.itemsPerPage', []);
this.lastPageLabel = this.i18n.get('paginator.lastPage', []);
this.nextPageLabel = this.i18n.get('paginator.nextPage', []);
this.previousPageLabel = this.i18n.get('paginator.previousPage', []);
}
getRangeLabel(page: number, pageSize: number, length: number): string {
if (length === 0) {
return this.i18n.get('paginator.empty', []);
}
const amountPages = Math.ceil(length / pageSize);
return this.i18n.get('paginator.range', [ page + 1 + "", amountPages + "" ]);
}
}
+26
View File
@@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { ReplaySubject, of } from 'rxjs';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class UserService {
constructor(private http: HttpClient) {
}
get() {
return this.http.get(environment.apiUrl + "/u");
}
getUser(username: string) {
return this.http.get(environment.apiUrl + "/u/" + username);
}
update(user: any) {
return this.http.post(environment.apiUrl + "/u", user);
}
}
+39
View File
@@ -0,0 +1,39 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class VoteService {
constructor(private http: HttpClient) {
}
getEntryPoints(target: number) {
return this.http.get(environment.apiUrl + "/v/e/" + target);
}
voteEntryUp(id: number) {
return this.http.put(environment.apiUrl + "/v/e/" + id, {});
}
voteEntryDown(id: number) {
return this.http.delete(environment.apiUrl + "/v/e/" + id, {});
}
getCommentPoints(target: number) {
return this.http.get(environment.apiUrl + "/v/c/" + target);
}
voteCommentUp(id: number) {
return this.http.put(environment.apiUrl + "/v/c/" + id, {});
}
voteCommentDown(id: number) {
return this.http.delete(environment.apiUrl + "/v/c/" + id, {});
}
}
+40
View File
@@ -0,0 +1,40 @@
<div mat-line>
<small>
<a *ngIf="comment.metadata && comment.metadata.vote" href="javascript:" (click)="voteUp(comment.id)"
matTooltip="{{'vote.up' | i18n}}">
<mat-icon inline="true">expand_less</mat-icon>
</a>
<mat-icon *ngIf="!comment.metadata || !comment.metadata.vote" inline="true">&nbsp;</mat-icon>
{{'comment.author' | i18n}}<a routerLink="/u/{{comment.author}}">{{comment.author}}</a>&nbsp;
<a routerLink="/c/{{comment.id}}" matTooltip="{{comment.created | datef:'LLLL'}}">{{comment.created
| datef}}</a>
<a *ngIf="comment.metadata && comment.metadata.downvote" href="javascript:" (click)="voteDown(comment.id)"
matTooltip="{{'vote.down' | i18n}}">
<mat-icon inline="true">remove</mat-icon>
</a>
</small>
</div>
<div mat-line>
{{comment.text}}
</div>
<div mat-line>
<small>
<a href="javascript:" (click)="comment.metadata.reply=!comment.metadata.reply">
{{(comment.metadata.reply ? 'comment.replyHide' : 'comment.reply') | i18n}}
</a>
<span *ngIf="comment.metadata && comment.metadata.unvote">|</span>
<a *ngIf="comment.metadata.unvote" href="javascript:" (click)="voteDown()">
{{'comment.unvote' | i18n}}
</a>
</small>
</div>
<div mat-line>
<ui-commentform *ngIf="comment.metadata.reply" [target]="comment.target" [parent]="comment.id"
[change]="boundReplyCallback"></ui-commentform>
</div>
<ng-container *ngIf="comment.metadata.comments">
<ui-comments [target]="comment.target" [parent]="comment.id"></ui-comments>
</ng-container>
+9
View File
@@ -0,0 +1,9 @@
small a {
color: inherit !important;
text-decoration: none;
}
small a:hover {
color: inherit !important;
text-decoration: underline;
}
+50
View File
@@ -0,0 +1,50 @@
import { Component, OnInit, Input } from '@angular/core';
import { VoteService } from '../../services/vote.service';
import { CommentService } from '../../services/comment.service';
@Component({
selector: 'ui-comment',
templateUrl: './comment.ui.html',
styleUrls: [ './comment.ui.scss' ]
})
export class UiComment implements OnInit {
@Input() comment: any;
@Input() change: Function;
boundReplyCallback: Function;
constructor(private commentService: CommentService, private voteService: VoteService) { }
ngOnInit(): void {
this.commentService.countParent(this.comment.target, this.comment.id).subscribe((data) => {
this.comment.metadata.comments = +data;
});
this.boundReplyCallback = this.replyCallback.bind(this);
}
voteUp() {
this.voteService.voteCommentUp(this.comment.id).subscribe((result) => {
this.change && this.change()
});
}
voteDown() {
this.voteService.voteCommentDown(this.comment.id).subscribe((result) => {
this.change && this.change()
});
}
author(author : string) {
return '<a href="/u/' + author + '">' + author + '</a>';
}
replyCallback(): void {
this.comment.metadata.reply = false;
this.comment.metadata.comments = 0;
this.commentService.countParent(this.comment.target, this.comment.id).subscribe((data) => {
this.comment.metadata.comments = +data;
});
}
}
@@ -0,0 +1 @@
<span *ngIf="count || count == 0">{{'entry.comments' | i18n:count}}</span>
@@ -0,0 +1,9 @@
a.datelink {
color: inherit !important;
text-decoration: none;
}
a.datelink:hover {
color: inherit !important;
text-decoration: underline;
}
@@ -0,0 +1,29 @@
import { Component, OnInit, Input } from '@angular/core';
import { CommentService } from '../../services/comment.service';
@Component({
selector: 'ui-commentcount',
templateUrl: './commentcount.ui.html',
styleUrls: [ './commentcount.ui.scss' ]
})
export class UiCommentCount implements OnInit {
count: number;
@Input() target: number;
@Input() parent: number;
constructor(private commentService: CommentService) { }
ngOnInit(): void {
if (this.target && this.parent) {
this.commentService.countParent(this.target, this.parent).subscribe((data) => {
this.count = +data;
});
} else if (this.target) {
this.commentService.count(this.target).subscribe((data) => {
this.count = +data;
});
}
}
}
@@ -0,0 +1,14 @@
<form [formGroup]="form" (ngSubmit)="create()" #formDirective="ngForm">
<mat-form-field>
<textarea matInput formControlName="text" placeholder="{{'comment.text' | i18n}}" required></textarea>
<mat-error *ngIf="hasError('text')">
{{'comment.text.error' | i18n}}
</mat-error>
</mat-form-field>
<button *ngIf="!working" mat-raised-button color="primary" [disabled]="form.invalid">
{{'comment.create' | i18n}}
</button>
</form>
@@ -0,0 +1,10 @@
mat-form-field {
display: block;
}
form {
max-width: 400px;
margin-left: 15px;
margin-bottom: 15px;
}
+43
View File
@@ -0,0 +1,43 @@
import { Component, OnInit, ViewChild, Input } from '@angular/core';
import { CommentService } from '../../services/comment.service';
import { FormBuilder, FormGroup, Validators, NgForm } from '@angular/forms';
@Component({
selector: 'ui-commentform',
templateUrl: './commentform.ui.html',
styleUrls: [ './commentform.ui.scss' ]
})
export class UiCommentForm implements OnInit {
@Input() target: number;
@Input() parent: number;
@Input() change: Function;
working: boolean = false;
form: FormGroup;
@ViewChild('formDirective') private formDirective: NgForm;
constructor(private commentService: CommentService,
private formBuilder: FormBuilder) { }
ngOnInit(): void {
this.form = this.formBuilder.group({
text: [ '', Validators.required ],
});
}
hasError(controlName: string): boolean {
return this.form.controls[ controlName ].errors != null;
}
create(): void {
const comment: any = {};
comment.target = this.target;
comment.parent = this.parent;
comment.text = this.form.get("text").value;
this.commentService.create(comment).subscribe((data) => {
this.form.reset();
this.change && this.change();
});
}
}
+6
View File
@@ -0,0 +1,6 @@
<div *ngIf="comments" fxLayout="column" fxFlexFill class="comments">
<ng-container *ngFor="let comment of comments.content; let i = index">
<mat-divider *ngIf="i > 0"></mat-divider>
<ui-comment [comment]="comment" [change]="boundRefresh"></ui-comment>
</ng-container>
</div>
+3
View File
@@ -0,0 +1,3 @@
.comments {
padding-left: 15px;
}
+36
View File
@@ -0,0 +1,36 @@
import { Component, OnInit, Input, Output } from '@angular/core';
import { CommentService } from '../../services/comment.service';
@Component({
selector: 'ui-comments',
templateUrl: './comments.ui.html',
styleUrls: [ './comments.ui.scss' ]
})
export class UiComments implements OnInit {
comments: any;
@Input() target: number;
@Input() parent: number;
boundRefresh: Function;
constructor(private commentService: CommentService) { }
ngOnInit(): void {
this.boundRefresh = this.refresh.bind(this);
this.refresh();
}
refresh(): void {
if (this.parent) {
this.commentService.getParent(this.target, this.parent).subscribe((data) => {
this.comments = data;
})
} else {
this.commentService.get(this.target).subscribe((data) => {
this.comments = data;
})
}
}
}
@@ -0,0 +1,7 @@
<mat-dialog-content>
{{text}}
</mat-dialog-content>
<mat-dialog-actions>
<button mat-raised-button [mat-dialog-close]="true" color="accent" matAutofocus>{{'confirm' | i18n}}</button>
<button mat-button [mat-dialog-close]="false">{{'cancel' | i18n}}</button>
</mat-dialog-actions>
@@ -0,0 +1,3 @@
mat-form-field {
display: block;
}
+21
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);
}
}
+21
View File
@@ -0,0 +1,21 @@
<mat-progress-bar *ngIf="!entries || !entries.content" mode="indeterminate"></mat-progress-bar>
<div *ngIf="entries && entries.content" fxLayout="column" fxFlexFill>
<mat-list flex-grow>
<ng-container *ngFor="let entry of entries.content; let i = index">
<mat-divider *ngIf="i > 0"></mat-divider>
<mat-list-item>
<ui-entry class="entry" [entry]="entry" [index]="i+1 + entries.number*entries.size" [change]="refresh">
</ui-entry>
</mat-list-item>
</ng-container>
</mat-list>
<p *ngIf="entries.totalElements == 0">{{'entries.nothing' | i18n}}</p>
<div fxFlexOffset="auto">
<mat-paginator *ngIf="entries.totalElements > 0" [pageSizeOptions]="pageSizeOptions" [pageIndex]="entries.number"
[length]="entries.totalElements" [pageSize]="entries.size" (page)="update && update($event)" showFirstLastButtons>
</mat-paginator>
</div>
</div>
+4
View File
@@ -0,0 +1,4 @@
.entry {
display: inline-block;
max-width: 100%;
}
+25
View File
@@ -0,0 +1,25 @@
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'ui-entries',
templateUrl: './entries.ui.html',
styleUrls: [ './entries.ui.scss' ]
})
export class UiEntries implements OnInit {
@Input() entries: any;
@Input() update: Function;
@Input() refresh: Function;
pageSizeOptions: number[] = [ 1, 2, 3, 4, 5, 10, 30, 50, 100 ];
constructor() { }
ngOnInit(): void {
}
}
+25
View File
@@ -0,0 +1,25 @@
<div mat-line>
<span *ngIf="index">{{index}}.&nbsp;</span>
<a *ngIf="entry.metadata && entry.metadata.vote" href="javascript:" (click)="voteUp(entry.id)"
matTooltip="{{'vote.up' | i18n}}">
<mat-icon inline="true">expand_less</mat-icon>
</a>
<mat-icon *ngIf="!entry.metadata || !entry.metadata.vote" inline="true">&nbsp;</mat-icon>
<mat-icon>{{'entryType.' + entry.entryType + '.icon' | i18n}}</mat-icon>&nbsp;
<a class="title" *ngIf="entry.url" [href]="entry.url" target="_blank">{{entry.title}}</a>
<a class="title" *ngIf="!entry.url" routerLink="/e/{{entry.id}}">{{entry.title}}</a>
</div>
<div mat-line>
<small>
{{'points' | i18n:(entry.metadata && entry.metadata.points)}}
{{'entry.author' | i18n}}<a routerLink="/u/{{entry.author}}">{{entry.author}}</a>&nbsp;
<a routerLink="/e/{{entry.id}}" matTooltip="{{entry.created | datef:'LLLL'}}">{{entry.created
| datef}}</a> |
<a *ngIf="entry.metadata && entry.metadata.unvote" href="javascript:" (click)="voteDown(entry.id)">
{{'entry.unvote' | i18n}}
</a> <span *ngIf="entry.metadata && entry.metadata.unvote">|</span>
<a routerLink="/e/{{entry.id}}">
{{'entry.comments' | i18n:(entry.metadata && entry.metadata.comments)}}
</a>
</small>
</div>
+9
View File
@@ -0,0 +1,9 @@
small a {
color: inherit !important;
text-decoration: none;
}
small a:hover {
color: inherit !important;
text-decoration: underline;
}
+32
View File
@@ -0,0 +1,32 @@
import { Component, OnInit, Input } from '@angular/core';
import { VoteService } from '../../services/vote.service';
@Component({
selector: 'ui-entry',
templateUrl: './entry.ui.html',
styleUrls: [ './entry.ui.scss' ]
})
export class UiEntry implements OnInit {
@Input() entry: any;
@Input() index : number;
@Input() change : Function;
constructor(private voteService: VoteService) { }
ngOnInit(): void {
}
voteUp() {
this.voteService.voteEntryUp(this.entry.id).subscribe((result) => {
this.change && this.change()
});
}
voteDown() {
this.voteService.voteEntryDown(this.entry.id).subscribe((result) => {
this.change && this.change()
});
}
}
View File
+43
View File
@@ -0,0 +1,43 @@
<mat-toolbar color="primary">
<mat-icon svgIcon="logo"></mat-icon>
<span>
{{'bstlboard' | i18n}}
</span>
<div *ngIf="auth && auth.authenticated">
<a mat-button routerLink="/">{{'top' | i18n}}</a>
<a mat-button routerLink="/new">{{'new' | i18n}}</a>
<a routerLink="/submit" mat-raised-button color="accent">{{'submission' |
i18n}}</a>
</div>
<span class="spacer"></span>
<ng-container>
<button mat-button [matMenuTriggerFor]="menu">
<mat-icon>settings</mat-icon>
<mat-icon>arrow_drop_down</mat-icon>
</button>
<mat-menu #menu="matMenu">
<a *ngIf="!auth || auth && !auth.authenticated" routerLink="/login" routerLinkActive="active" mat-menu-item>
<mat-icon>login</mat-icon> {{'login' | i18n}}
</a>
<a *ngIf="auth && auth.authenticated" routerLink="/settings" routerLinkActive="active" mat-menu-item>
<mat-icon>tune</mat-icon> {{'settings' | i18n}}
</a>
<mat-divider></mat-divider>
<a *ngFor="let locale of locales" mat-menu-item (click)="setLocale(locale)">{{'locale.' + locale + '.long' |
i18n}} <mat-icon inline=true *ngIf="locale == currentLocale">done</mat-icon></a>
<a mat-menu-item>
<mat-slide-toggle (change)="darkThemeChange($event)" [checked]="darkTheme == 'true'">
{{'darkTheme' | i18n}}
</mat-slide-toggle>
</a>
<mat-divider *ngIf="auth && auth.authenticated"></mat-divider>
<a *ngIf="auth && auth.authenticated" (click)="logout()" mat-menu-item>
<mat-icon>exit_to_app</mat-icon> {{'logout' | i18n}}
</a>
</mat-menu>
</ng-container>
</mat-toolbar>
<div class="container" fxFlex>
<router-outlet></router-outlet>
</div>
+126
View File
@@ -0,0 +1,126 @@
import { Component, HostListener } from '@angular/core';
import { AuthService } from '../../services/auth.service';
import { UserService } from '../../services/user.service';
import { I18nService } from '../../services/i18n.service';
import { Router } from '@angular/router';
import { DomSanitizer } from '@angular/platform-browser';
import { MatIconRegistry } from '@angular/material/icon';
import { DateAdapter } from '@angular/material/core';
@Component({
selector: 'ui-main',
templateUrl: './main.ui.html'
})
export class UiMain {
opened = true;
darkTheme = "false";
title = 'bstlboard';
currentLocale: String;
datetimeformat: String;
locales;
auth;
constructor(
private i18n: I18nService,
private authService: AuthService,
private userService: UserService,
private router: Router,
private iconRegistry: MatIconRegistry,
private sanitizer: DomSanitizer,
private _adapter: DateAdapter<any>) {
iconRegistry.addSvgIcon('logo', sanitizer.bypassSecurityTrustResourceUrl('assets/icons/logo.svg'));
}
ngOnInit() {
this.datetimeformat = this.i18n.get('format.datetime', []);
this.currentLocale = this.i18n.getLocale();
this.locales = this.i18n.getLocales();
this.authService.auth.subscribe(data => {
this.auth = data;
})
this._adapter.setLocale(this.currentLocale);
const width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
if (width < 768) {
this.opened = false;
} else {
this.opened = true;
}
if (localStorage.getItem("bstlboard.darkTheme") == "true") {
this.darkTheme = "true";
window.document.body.classList.add("dark-theme");
}
}
setLocale(locale) {
localStorage.setItem("bstlboard.locale", locale);
if (this.auth && this.auth.authenticated) {
this.userService.get().subscribe((user: any) => {
user.locale = locale;
this.userService.update(user).subscribe(() => {
window.location.reload();
})
});
} else {
window.location.reload();
}
}
darkThemeChange($event) {
if ($event.checked) {
this.darkTheme = "true";
} else {
this.darkTheme = "false";
}
localStorage.setItem("bstlboard.darkTheme", this.darkTheme);
if (this.auth && this.auth.authenticated) {
this.userService.get().subscribe((user: any) => {
user.darkTheme = $event.checked;
this.userService.update(user).subscribe(() => {
window.location.reload();
})
});
} else {
window.location.reload();
}
}
logout() {
this.authService.logout().subscribe(data => {
this.router.navigate([ "" ]).then(() => {
window.location.reload();
});
})
}
isBiggerScreen() {
const width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
if (width < 768) {
return false;
} else {
return true;
}
}
openExternal(url, target = '_self') {
window.open(url, target);
}
@HostListener('window:resize', [ '$event' ])
onResize(event) {
if (event.target.innerWidth < 768) {
this.opened = false;
} else {
this.opened = true;
}
}
}
+1
View File
@@ -0,0 +1 @@
<span *ngIf="count || count == 0">{{'points' | i18n:count}}</span>
View File
+29
View File
@@ -0,0 +1,29 @@
import { Component, OnInit, Input } from '@angular/core';
import { VoteService } from '../../services/vote.service';
@Component({
selector: 'ui-points',
templateUrl: './points.ui.html',
styleUrls: [ './points.ui.scss' ]
})
export class UiPoints implements OnInit {
count: number;
@Input() target: number;
@Input() type: string;
constructor(private voteService: VoteService) { }
ngOnInit(): void {
if (this.type == 'e') {
this.voteService.getEntryPoints(this.target).subscribe((data) => {
this.count = +data;
});
} else if (this.type == 'c') {
this.voteService.getCommentPoints(this.target).subscribe((data) => {
this.count = +data;
});
}
}
}
+18
View File
@@ -0,0 +1,18 @@
import { Pipe, PipeTransform } from '@angular/core';
import { I18nService } from './../services/i18n.service';
@Pipe({
name: 'i18n'
})
export class I18nPipe implements PipeTransform {
constructor(private i18n: I18nService) {
}
transform(value: String, ...args: any[]): String {
return this.i18n.get(value, args);
}
}
+22
View File
@@ -0,0 +1,22 @@
import { FormGroup } from '@angular/forms';
export function MatchingValidator(passwordName: string, password2Name: string) {
return (formGroup: FormGroup) => {
const password = formGroup.controls[passwordName];
const password2 = formGroup.controls[password2Name];
if (password2.errors && !password2.errors.matchingValidator) {
return;
}
if (password.value !== password2.value) {
password2.setErrors({ matchingValidator: true });
} else {
password2.setErrors(null);
}
}
}
+13
View File
@@ -0,0 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core';
import * as moment from 'moment';
@Pipe({ name: 'datef' })
export class MomentPipe implements PipeTransform {
transform(value: Date | moment.Moment, dateFormat: string): any {
if (!dateFormat) {
return moment(value).fromNow();
}
return moment(value).format(dateFormat);
}
}
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

+183
View File
@@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="180mm"
height="180mm"
viewBox="0 0 637.79528 637.79527"
id="svg2"
version="1.1"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="logo.svg"
inkscape:export-filename="/home/lurkars/ownCloud/Bastelei/development/bstl_ui/build/gfx/bstl.png"
inkscape:export-xdpi="24.190475"
inkscape:export-ydpi="24.190475">
<defs
id="defs4">
<marker
inkscape:stockid="Arrow1Lend"
orient="auto"
refY="0"
refX="0"
id="Arrow1Lend"
style="overflow:visible"
inkscape:isstock="true">
<path
id="path4410"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
style="fill:#7560ff;fill-opacity:1;fill-rule:evenodd;stroke:#7560ff;stroke-width:1.00000003pt;stroke-opacity:1"
transform="matrix(-0.8,0,0,-0.8,-10,0)"
inkscape:connector-curvature="0" />
</marker>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="0.98994949"
inkscape:cx="-75.221162"
inkscape:cy="305.4019"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1848"
inkscape:window-height="1016"
inkscape:window-x="72"
inkscape:window-y="27"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-49.288974,-360.43866)"
style="display:inline">
<g
id="g4145"
style="stroke:#ebb400;stroke-width:20;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="translate(0,-15.000001)">
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path4154"
d="m 468.57022,631.84443 -42.94501,131.608"
style="fill:none;fill-rule:evenodd;stroke:#ebb400;stroke-width:20;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path4156"
d="M 355.63121,547.02142 660.78242,768.72229"
style="fill:none;fill-rule:evenodd;stroke:#ebb400;stroke-width:20;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
<path
style="fill:none;fill-rule:evenodd;stroke:#006100;stroke-width:20;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 563.03718,392.90301 492.79039,606.93332 674.34654,739.73223"
id="path4158"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccc" />
<path
style="fill:none;fill-rule:evenodd;stroke:#5e45ff;stroke-width:20;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 546.82821,387.53706 -286.72601,207.38363 44.34514,139.99727 125.28725,-0.48581 -72.0052,221.3299"
id="path4152"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<g
id="g4141"
style="stroke:#c1008d;stroke-width:20;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="translate(0,-15.000001)">
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path4150"
d="M 85.517039,769.35462 298.59321,768.46556"
style="fill:none;fill-rule:evenodd;stroke:#c1008d;stroke-width:20;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cccc"
inkscape:connector-curvature="0"
id="path4148"
d="M 347.86433,949.1813 89.095578,757.90563 249.71676,642.08318 Z"
style="fill:none;fill-rule:evenodd;stroke:#c1008d;stroke-width:20;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
<path
sodipodi:type="star"
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:18.2419529;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4138"
sodipodi:sides="5"
sodipodi:cx="360"
sodipodi:cy="583.79077"
sodipodi:r1="294.93686"
sodipodi:r2="238.60893"
sodipodi:arg1="0.95054684"
sodipodi:arg2="1.5788654"
inkscape:flatsided="true"
inkscape:rounded="0"
inkscape:randomized="0"
d="M 531.42857,823.79077 184.72078,820.9931 80.242919,490.38988 362.37983,288.86351 641.2279,494.91659 Z"
inkscape:transform-center-x="0.32354069"
inkscape:transform-center-y="30.570376"
transform="matrix(-1.096361,0.00529662,-0.00529662,-1.096361,766.69563,1285.6526)" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="template"
style="display:none"
transform="translate(-49.288974,-52.170936)">
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 663.78063,451.04608 185.86807,102.14254"
id="path4446"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 551.54329,102.64762 71.720831,450.1401"
id="path4448"
inkscape:connector-curvature="0" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 186.37315,102.14255 180.8173,564.9278"
id="path4450"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 551.54329,103.15269 366.93791,666.81782"
id="path4452"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 72.730983,451.15025 590.686697,0.25254"
id="path4454"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

+4
View File
@@ -0,0 +1,4 @@
export const environment = {
production: true,
apiUrl : 'https://api.bstly.de'
};
+18
View File
@@ -0,0 +1,18 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false,
apiUrl : 'http://localhost:8080/api',
//apiUrl : 'https://brd.bstly.lh8.de/api',
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>bstlboard</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="assets/icons/favicon.png">
</head>
<body>
<app-root></app-root>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
+67
View File
@@ -0,0 +1,67 @@
/***************************************************************************************************
* Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
*/
import '@angular/localize/init';
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
+335
View File
@@ -0,0 +1,335 @@
// Custom Theming for Angular Material
// For more information: https://material.angular.io/guide/theming
@import '~@angular/material/theming';
// Plus imports for other components in your app.
// Include the common styles for Angular Material. We include this here so that you only
// have to load a single css file for Angular Material in your app.
// Be sure that you only ever include this mixin once!
@include mat-core();
@import './variables.scss';
// Define the palettes for your theme using the Material Design palettes available in palette.scss
// (imported above). For each palette, you can optionally specify a default, lighter, and darker
// hue. Available color palettes: https://material.io/design/color/
$light-theme: mat-light-theme((color: (primary: $light-primary,
accent: $light-accent,
warn: $light-warn,
)));
// Define an alternate dark theme.
$dark-theme: mat-dark-theme((color: (primary: $dark-primary,
accent: $light-accent,
warn: $light-warn,
)));
// Include theme styles for core and each component used in your app.
// Alternatively, you can import and @include the theme mixins for each component
// that you are using.
@include angular-material-theme($light-theme);
.dark-theme {
@include angular-material-color($dark-theme);
}
/* You can add global styles to this file, and also import other style files */
/* fallback */
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(assets/fonts/material_icons.woff2) format('woff2');
}
a {
color: $primary;
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-moz-font-feature-settings: 'liga';
-moz-osx-font-smoothing: grayscale;
}
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html,
body {
height: 100%;
max-height: 100%;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important;
}
app-root, ui-main {
height: 100%;
max-height: 100%;
display: flex;
flex-direction: column;
}
app-root {
padding: 15px;
background-color: #fafafa;
}
ui-main {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
mat-card {
max-width: 400px;
margin: 2em auto;
}
mat-form-field {
display: block;
}
mat-form-field {
&.ng-valid {
.mat-form-field-wrapper {
padding-bottom: 1.25em;
}
}
&.ng-invalid,
&.mat-form-field-invalid {
.mat-form-field-wrapper {
padding-bottom: 7px;
}
}
&.ng-untouched {
.mat-form-field-wrapper {
padding-bottom: 1.25em;
}
}
.mat-form-field {
&-underline {
position: static;
}
&-subscript-wrapper {
position: static;
}
}
}
qrcode {
margin: 0 auto;
text-align: center;
}
qrcode canvas {
width: 100% !important;
height: auto !important;
max-width: 400px !important;
}
.spacer {
flex: 1 1 auto;
}
.hint {
opacity: 0.7;
}
.mat-drawer-inner-container {
display: flex;
flex-direction: column;
}
mat-sidenav-container {
height: 100%;
max-height: 100%;
}
.container {
width: 100%;
max-width: 100%;
padding-right: 2px;
padding-left: 2px;
margin-right: auto;
margin-left: auto;
margin-top: 15px;
margin-bottom: 15px;
overflow-x: none;
overflow-y: auto;
@media screen and (min-width: 576px) {
padding-right: 3px;
padding-left: 3px;
}
@media screen and (min-width: 768px) {
padding-right: 15px;
padding-left: 15px;
}
@media screen and (min-width: 992px) {
padding-right: 25px;
padding-left: 25px;
}
@media screen and (min-width: 1200px) {
padding-right: 45px;
padding-left: 45px;
}
}
.text-center {
text-align: center;
}
.text-justify {
text-align: justify;
}
.text-right {
text-align: right;
}
.text-warning {
color: $warn;
}
.align-right {
display: flex;
padding: 21px 0;
justify-content: flex-end;
}
.mat-tooltip-trigger {
cursor: pointer;
}
mat-card.warn,
mat-card.accent {
padding: 0;
mat-card-content {
padding: 16px;
}
mat-card-header {
padding: 16px;
padding-bottom: 0;
}
mat-card-actions {
padding: 16px !important;
padding-top: 0 !important;
}
}
mat-card.warn mat-card-header {
background-color: $warn !important;
}
mat-card.accent mat-card-header {
background-color: $accent !important;
}
table {
border: 0;
border-spacing: 0;
width: 100%;
background: white;
th,
td,
td {
color: rgba(0, 0, 0, 0.87);
font-size: 14px;
border: 0;
padding: 14px;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: rgba(0, 0, 0, 0.12);
}
th:first-of-type,
td:first-of-type,
td:first-of-type {
padding-left: 24px;
}
thead {
tr {
height: 56px;
th {
color: rgba(0, 0, 0, 0.54);
font-size: 12px;
font-weight: 500;
}
}
}
}
.dark-theme {
app-root {
background-color: #303030;
}
table {
background: #424242;
th,
td,
td {
color: white;
border-bottom-color: rgba(255, 255, 255, 0.12);
}
thead {
tr {
th {
color: rgba(255, 255, 255, 0.7);
}
}
}
}
}
.help-button {
float: right;
position: relative;
top: -40px;
right: 15px;
}
+25
View File
@@ -0,0 +1,25 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);
+19
View File
@@ -0,0 +1,19 @@
@import '~@angular/material/theming';
$light-primary: mat-palette($mat-deep-orange, 800);
$light-accent: mat-palette($mat-grey, A200, A100, A400);
$light-warn: mat-palette($mat-red);
$primary: mat-color($light-primary);
$accent: mat-color($light-accent);
$warn: mat-color($light-warn);
$dark-primary: mat-palette($mat-deep-orange, 900, 500, 700);
$dark-accent: mat-palette($mat-grey, A200, A100, A400);
$dark-warn: mat-palette($mat-red);
.dark-theme {
$primary: mat-color($light-primary);
$accent: mat-color($light-accent);
$warn: mat-color($light-warn);
}