initial commit
This commit is contained in:
@@ -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]
|
||||
@@ -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 { }
|
||||
@@ -0,0 +1,3 @@
|
||||
<mat-drawer-container>
|
||||
<router-outlet></router-outlet>
|
||||
</mat-drawer-container>
|
||||
@@ -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!');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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 });
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,8 @@
|
||||
mat-form-field {
|
||||
display: block;
|
||||
}
|
||||
|
||||
a.external-login {
|
||||
margin: 15px 0;
|
||||
display: block;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<ui-entries [entries]="entries" [refresh]="boundRefresh" [update]="boundUpdate"></ui-entries>
|
||||
@@ -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) => { })
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 ]);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<ui-entries [entries]="entries" [refresh]="boundRefresh" [update]="boundUpdate"></ui-entries>
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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", {});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "" ]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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, {});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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"> </mat-icon>
|
||||
{{'comment.author' | i18n}}<a routerLink="/u/{{comment.author}}">{{comment.author}}</a>
|
||||
<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>
|
||||
@@ -0,0 +1,9 @@
|
||||
small a {
|
||||
color: inherit !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
small a:hover {
|
||||
color: inherit !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
.comments {
|
||||
padding-left: 15px;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
.entry {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<div mat-line>
|
||||
<span *ngIf="index">{{index}}. </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"> </mat-icon>
|
||||
<mat-icon>{{'entryType.' + entry.entryType + '.icon' | i18n}}</mat-icon>
|
||||
<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>
|
||||
<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>
|
||||
@@ -0,0 +1,9 @@
|
||||
small a {
|
||||
color: inherit !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
small a:hover {
|
||||
color: inherit !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -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()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<span *ngIf="count || count == 0">{{'points' | i18n:count}}</span>
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
@@ -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 |
@@ -0,0 +1,4 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
apiUrl : 'https://api.bstly.de'
|
||||
};
|
||||
@@ -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.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 948 B |
@@ -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
@@ -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));
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user