From e24d99359c7784ce0b42bcb064ce62399a7eb75a Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 <52419336+IchHabeHunger54@users.noreply.github.com> Date: Sun, 7 Mar 2021 18:08:38 +0100 Subject: [PATCH 1/7] Create TODO.md --- TODO.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index 599e81e..cd1971a 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,4 @@ -# Menu +# Admin Interface - Menu -- [ ] Two-layered navigation menu +- [ ] Add new site/category +- [ ] Move site/category From 7095e9fabb674c86c4d53a71a4aad2244f5840eb Mon Sep 17 00:00:00 2001 From: Georg Burkl Date: Thu, 6 May 2021 12:12:03 +0200 Subject: [PATCH 2/7] bring common cod on par with other branches Signed-off-by: Minecraftschurli --- src/app/app-routing.module.ts | 2 +- src/app/app.component.ts | 8 +- src/app/auth/auth.service.ts | 28 ++--- src/app/core/config/app-config.ts | 6 +- src/app/core/core.module.ts | 4 +- src/app/core/error.interceptor.ts | 4 + src/app/core/json.interceptor.spec.ts | 16 +++ src/app/core/json.interceptor.ts | 57 +++++++++ src/app/data/category.ts | 26 +++- src/app/data/change.ts | 32 +++++ src/app/data/event.ts | 16 +++ src/app/data/gallery.ts | 8 ++ src/app/data/login-result.ts | 14 ++- src/app/data/media.ts | 13 ++ src/app/data/page.ts | 27 +++- src/app/data/upload-result.ts | 5 + src/app/data/user.ts | 24 +++- .../shared/services/content.service.spec.ts | 5 +- src/app/shared/services/content.service.ts | 115 ++++++++++++++++-- src/app/shared/services/event.service.spec.ts | 18 +++ src/app/shared/services/event.service.ts | 61 ++++++++++ src/app/shared/services/media.service.spec.ts | 19 +++ src/app/shared/services/media.service.ts | 20 +++ src/assets/config/config.dev.json | 2 +- src/index.html | 3 +- src/styles.scss | 11 ++ src/theme.scss | 25 +++- 27 files changed, 519 insertions(+), 50 deletions(-) create mode 100644 src/app/core/json.interceptor.spec.ts create mode 100644 src/app/core/json.interceptor.ts create mode 100644 src/app/data/change.ts create mode 100644 src/app/data/event.ts create mode 100644 src/app/data/gallery.ts create mode 100644 src/app/data/media.ts create mode 100644 src/app/data/upload-result.ts create mode 100644 src/app/shared/services/event.service.spec.ts create mode 100644 src/app/shared/services/event.service.ts create mode 100644 src/app/shared/services/media.service.spec.ts create mode 100644 src/app/shared/services/media.service.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 06c7342..508a10c 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; -const routes: Routes = []; +const routes: Routes = [{ path: 'admin', loadChildren: () => import('./features/admin/admin.module').then(m => m.AdminModule) }]; @NgModule({ imports: [RouterModule.forRoot(routes)], diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 0d9efa9..b4d6d5f 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,9 +1,11 @@ -import { Component } from '@angular/core'; +import {Component, HostBinding} from '@angular/core'; @Component({ - selector: 'app-root', + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'body', templateUrl: './app.component.html' }) export class AppComponent { - title = 'Pfarre-Machstrasse'; + @HostBinding('class') class = 'mat-typography'; + title = 'Pfarre Machstrasse - Hl. Klaus von Flüe'; } diff --git a/src/app/auth/auth.service.ts b/src/app/auth/auth.service.ts index ca74b11..276fd5d 100644 --- a/src/app/auth/auth.service.ts +++ b/src/app/auth/auth.service.ts @@ -49,7 +49,7 @@ export class AuthService implements OnDestroy { public login(username: string, password: string): Observable { if (!environment.production) { - this._user$.next({username, role: 'admin'}); + this._user$.next({id: '', email: username, firstName: 'Demo', lastName: 'User', role: 'admin'}); return of(true); } return this._http @@ -83,8 +83,8 @@ export class AuthService implements OnDestroy { } public setLocalStorage(x: LoginResult): void { - localStorage.setItem(ACCESS_TOKEN, x.access_token); - localStorage.setItem(REFRESH_TOKEN, x.refresh_token ?? null); + localStorage.setItem(ACCESS_TOKEN, x.accessToken); + localStorage.setItem(REFRESH_TOKEN, x.refreshToken ?? null); localStorage.setItem('login-event', 'login' + Math.random()); } @@ -94,17 +94,7 @@ export class AuthService implements OnDestroy { localStorage.setItem('logout-event', 'logout' + Math.random()); } - private getTokenRemainingTime(): number { - const accessToken = this.getAccessToken(); - if (!accessToken) { - return 0; - } - const jwtToken = JSON.parse(atob(accessToken.split('.')[1])); - const expires = new Date(jwtToken.exp * 1000); - return expires.getTime() - Date.now(); - } - - private refreshToken(): Observable { + public refreshToken(): Observable { const refreshToken = this.getRefreshToken(); if (!refreshToken) { this.doLogout(); @@ -123,6 +113,16 @@ export class AuthService implements OnDestroy { ); } + private getTokenRemainingTime(): number { + const accessToken = this.getAccessToken(); + if (!accessToken) { + return 0; + } + const jwtToken = JSON.parse(atob(accessToken.split('.')[1])); + const expires = new Date(jwtToken.exp * 1000); + return expires.getTime() - Date.now(); + } + private updateUser(): void { this._http.get(this.userUrl).subscribe(user => this._user$.next(user)); } diff --git a/src/app/core/config/app-config.ts b/src/app/core/config/app-config.ts index 86f7eab..68fbffe 100644 --- a/src/app/core/config/app-config.ts +++ b/src/app/core/config/app-config.ts @@ -6,11 +6,15 @@ export interface IConfig { apiEndpoint: string; } +const NULL_CONFIG: IConfig = { + apiEndpoint: null +}; + @Injectable() export class AppConfig { private static _config: IConfig; public static get INSTANCE(): IConfig { - return AppConfig._config; + return AppConfig._config ?? NULL_CONFIG; } constructor(private _http: HttpClient) {} load(): Promise { diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index a8a18eb..596d98c 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common'; import { LoggingModule } from './logging/logging.module'; import {ErrorInterceptor} from './error.interceptor'; import {AppConfig} from './config/app-config'; +import {JsonInterceptor} from './json.interceptor'; @NgModule({ @@ -16,7 +17,8 @@ import {AppConfig} from './config/app-config'; ], providers: [ AppConfig, - ErrorInterceptor + ErrorInterceptor, + JsonInterceptor ] }) export class CoreModule { diff --git a/src/app/core/error.interceptor.ts b/src/app/core/error.interceptor.ts index 60da253..1197796 100644 --- a/src/app/core/error.interceptor.ts +++ b/src/app/core/error.interceptor.ts @@ -30,6 +30,10 @@ export class ErrorInterceptor implements HttpInterceptor { this._router.navigate(['']); this._logger.error('Authentication error {0}', err?.message ?? err?.error?.message ?? err?.statusText); } + if (err.status === 404) { + this._router.navigate(['']); + this._logger.error('Not found error {0}', err?.message ?? err?.error?.message ?? err?.statusText); + } const error = err?.message ?? err?.error?.message ?? err?.statusText; return throwError(error); }) diff --git a/src/app/core/json.interceptor.spec.ts b/src/app/core/json.interceptor.spec.ts new file mode 100644 index 0000000..674c584 --- /dev/null +++ b/src/app/core/json.interceptor.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { JsonInterceptor } from './json.interceptor'; + +describe('JsonInterceptor', () => { + beforeEach(() => TestBed.configureTestingModule({ + providers: [ + JsonInterceptor + ] + })); + + it('should be created', () => { + const interceptor: JsonInterceptor = TestBed.inject(JsonInterceptor); + expect(interceptor).toBeTruthy(); + }); +}); diff --git a/src/app/core/json.interceptor.ts b/src/app/core/json.interceptor.ts new file mode 100644 index 0000000..0d3caa4 --- /dev/null +++ b/src/app/core/json.interceptor.ts @@ -0,0 +1,57 @@ +import { Injectable } from '@angular/core'; +import { + HttpRequest, + HttpHandler, + HttpEvent, + HttpInterceptor, HttpResponse +} from '@angular/common/http'; +import { Observable } from 'rxjs'; +import {map} from 'rxjs/operators'; + +const isArray = Array.isArray; + +const isObject = (o) => o === Object(o) && !isArray(o) && typeof o !== 'function'; + +const toCamel = (s) => s.replace(/([-_][a-z])/ig, ($1) => $1.toUpperCase() + .replace('-', '') + .replace('_', '')); + +const keysToCamel = (o) => { + if (isObject(o)) { + const n = {}; + + Object.keys(o) + .forEach((k) => { + n[toCamel(k)] = keysToCamel(o[k]); + }); + + return n; + } else if (isArray(o)) { + return o.map((i) => keysToCamel(i)); + } + + return o; +}; + +/** + * Intercept json responses and convert them to camelCase + */ +@Injectable() +export class JsonInterceptor implements HttpInterceptor { + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + if (request.responseType === 'json') { + return next.handle(request).pipe(map(value => { + if (value instanceof HttpResponse) { + try { + return value.clone({body: keysToCamel(value.body)}); + } catch (ignored) { + } + } + return value; + })); + } else { + return next.handle(request); + } + } +} diff --git a/src/app/data/category.ts b/src/app/data/category.ts index 8abaa8e..cad652e 100644 --- a/src/app/data/category.ts +++ b/src/app/data/category.ts @@ -1,13 +1,33 @@ import {Observable} from 'rxjs'; import {Page} from './page'; +/** + * The DTO (DataTransferObject) for a category + */ export interface CategoryDTO { + /** + * The id of this category + */ id: string; + /** + * The title of this category + */ title: string; + /** + * The ordering number of this category + */ + order: number; } -export interface Category { - id: string; - title: string; +/** + * The full category object + */ +export interface Category extends CategoryDTO { + /** + * The direct accessor for all pages of this category
+ * Executes a http request to fetch the pages + * + * @see Page + */ pages$?: Observable; } diff --git a/src/app/data/change.ts b/src/app/data/change.ts new file mode 100644 index 0000000..f20079d --- /dev/null +++ b/src/app/data/change.ts @@ -0,0 +1,32 @@ +export type ChangeType = 1 | 0 | -1; +export type ChangeData = ([ChangeType, string])[]; + +export interface Change { + /** + * The UUID of the User creating this change + * + * @type string uuid + */ + author: string; + /** + * The id of the category which the page of this change belongs to + * + * @see Category.id + * @see Page.category + */ + category: string; + /** + * The id of the page this change was made on + * + * @see Page.id + */ + page: string; + /** + * The timestamp of creation of this change + */ + createdAt: Date; + /** + * The data of this change + */ + data: ChangeData; +} diff --git a/src/app/data/event.ts b/src/app/data/event.ts new file mode 100644 index 0000000..db2b456 --- /dev/null +++ b/src/app/data/event.ts @@ -0,0 +1,16 @@ +import {Observable} from 'rxjs'; +import {Media} from './media'; + +export interface EventDTO { + id: string; + name: string; + details: string; + start: Date; + end: Date; + media: string | null; + owner: string; +} + +export interface Event extends EventDTO { + media$: Observable; +} diff --git a/src/app/data/gallery.ts b/src/app/data/gallery.ts new file mode 100644 index 0000000..d49b7a7 --- /dev/null +++ b/src/app/data/gallery.ts @@ -0,0 +1,8 @@ +import {Media} from './media'; + +export interface Gallery { + id: string; + title: string; + owner: string; + media: Media[]; +} diff --git a/src/app/data/login-result.ts b/src/app/data/login-result.ts index c917030..4eab0ac 100644 --- a/src/app/data/login-result.ts +++ b/src/app/data/login-result.ts @@ -1,5 +1,13 @@ -/* eslint-disable @typescript-eslint/naming-convention */ +/** + * The login result containing the JWT tokens for access and refresh + */ export interface LoginResult { - access_token: string; - refresh_token?: string; + /** + * The JWT token used to authenticate with the backend + */ + accessToken: string; + /** + * The JWT token used to refresh the access token + */ + refreshToken?: string; } diff --git a/src/app/data/media.ts b/src/app/data/media.ts new file mode 100644 index 0000000..28ac68c --- /dev/null +++ b/src/app/data/media.ts @@ -0,0 +1,13 @@ +export interface MediaLinks { + file: string; + thumbnail: string; +} + +export interface Media { + _links: MediaLinks; + id: string; //uuid + name: string; + mimetype: string; + extension: string; + owner: string; //uuid +} diff --git a/src/app/data/page.ts b/src/app/data/page.ts index 78b1b8a..819f5d8 100644 --- a/src/app/data/page.ts +++ b/src/app/data/page.ts @@ -1,12 +1,33 @@ import {Observable} from 'rxjs'; +/** + * The DTO (DataTransferObject) for a page + */ export interface PageDTO { + /** + * The id of this page + */ id: string; + /** + * The title of this page + */ title: string; + /** + * The category this page belongs to + */ + category: string; + /** + * The position of the page insede the category + */ + order: number; } -export interface Page { - id: string; - title: string; +/** + * The full page object + */ +export interface Page extends PageDTO { + /** + * The direct accessor for the content of this page + */ content$: Observable; } diff --git a/src/app/data/upload-result.ts b/src/app/data/upload-result.ts new file mode 100644 index 0000000..8dce90b --- /dev/null +++ b/src/app/data/upload-result.ts @@ -0,0 +1,5 @@ +export interface UploadResult { + name: string; + url: string; + media: boolean; +} diff --git a/src/app/data/user.ts b/src/app/data/user.ts index dd02b8d..6495d1d 100644 --- a/src/app/data/user.ts +++ b/src/app/data/user.ts @@ -1,5 +1,27 @@ export type Role = 'admin' | 'author'; + +/** + * The object representing a user + */ export interface User { - username: string; + /** + * The UUID of the user + */ + id: string; + /** + * The users email + */ + email: string; + /** + * The users first name + */ + firstName: string; + /** + * The users last name + */ + lastName: string; + /** + * The users role + */ role: Role; } diff --git a/src/app/shared/services/content.service.spec.ts b/src/app/shared/services/content.service.spec.ts index 6ca2a32..76e3a37 100644 --- a/src/app/shared/services/content.service.spec.ts +++ b/src/app/shared/services/content.service.spec.ts @@ -1,12 +1,15 @@ import { TestBed } from '@angular/core/testing'; import { ContentService } from './content.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; describe('ContentService', () => { let service: ContentService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule] + }); service = TestBed.inject(ContentService); }); diff --git a/src/app/shared/services/content.service.ts b/src/app/shared/services/content.service.ts index 81f5681..f02a3d9 100644 --- a/src/app/shared/services/content.service.ts +++ b/src/app/shared/services/content.service.ts @@ -1,21 +1,114 @@ import { Injectable } from '@angular/core'; -import {Observable, of} from 'rxjs'; -import {Category} from '../../data/category'; +import { UploadResult } from 'src/app/data/upload-result'; +import {Observable, of, throwError} from 'rxjs'; +import {Category, CategoryDTO} from '../../data/category'; +import {Page, PageDTO} from '../../data/page'; +import {HttpClient} from '@angular/common/http'; +import {AppConfig} from '../../core/config/app-config'; +import {map} from 'rxjs/operators'; -const MOCK = []; -for (let i = 0; i < 5; i++) { - const pages = []; - for (let j = 0; j < 5; j++) { - pages.push({id: `mock${j}`, title: `Page${j}`, content$: of('')}); - } - MOCK.push({id: `mock${i}`, title: `Category${i}`, pages$: of(pages)}); -} +const HOME = ` +Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab aliquid aspernatur, autem consectetur consequatur +deleniti eligendi enim et modi molestias mollitia natus necessitatibus nobis nulla numquam, odit omnis pariatur +perferendis possimus quae qui quia quibusdam quo saepe sit voluptas voluptatem! Aliquam at aut consequatur culpa +dignissimos itaque libero minima, nemo nobis porro praesentium, quidem rem repudiandae sapiente sunt temporibus vel +veniam voluptates! Animi necessitatibus quaerat quisquam totam ullam voluptatibus. Adipisci, aliquid amet aperiam +atque consequatur distinctio dolore excepturi facilis incidunt laboriosam laborum maxime minus molestiae nesciunt +nihil optio pariatur quo similique! Ab aut dolor, labore nobis perspiciatis quaerat rem ullam! + +Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error, illum. + +Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet dolorum incidunt saepe voluptate. +Deserunt, facilis. +Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ducimus enim iure magni maxime +mollitia repellendus. +Aperiam expedita explicabo molestias neque qui ut veniam. Blanditiis cumque doloribus +mollitia quas qui sequi. +Accusantium, animi dolorum facere hic illum, ipsa iste laudantium nam nihil quisquam +quod vitae voluptate? + +Lorem ipsum dolor sit amet, consectetur adipisicing elit. Iure, soluta? +`; @Injectable({ providedIn: 'root' }) export class ContentService { + private readonly _url: string; + + constructor(private _http: HttpClient) { + this._url = AppConfig.INSTANCE.apiEndpoint; + } + public getCategories(): Observable { - return of(MOCK); + return this._http.get(`${this._url}/category`) + .pipe(map(this.dtoToCategory.bind(this))); + } + + public getCategory(categoryId: string): Observable { + return this._http.get(`${this._url}/category/${categoryId}`) + .pipe(map(this.dtoToCategory.bind(this))); + } + + public getPages(categoryId: string): Observable { + return this._http.get(`${this._url}/category/${categoryId}/page`) + .pipe(map(this.dtoToPage.bind(this))); + } + + public getPage(categoryId: string, pageId: string): Observable { + return this._http.get(`${this._url}/category/${categoryId}/page/${pageId}`) + .pipe(map(this.dtoToPage.bind(this))); + } + + public getPageContent(categoryId: string, pageId: string): Observable { + return this._http.get(`${this._url}/category/${categoryId}/page/${pageId}/content`); + } + + public getHomeContent(): Observable { + return of(HOME); + } + + public uploadFile(file: File): Promise { + return new Promise(resolve => { + resolve({ + name: file.name, + url: '', + media: file.type.startsWith('image') || file.type.startsWith('video') || file.type.startsWith('audio') + }); + }); + } + + public saveContent(categoryId: string, pageId: string, content: string): Observable { + return this._http.put(`${this._url}/category/${categoryId}/page/${pageId}/content`, content); + } + + private dtoToCategory(dto: (CategoryDTO | CategoryDTO[])): (Category | Category[]) { + if (Array.isArray(dto)) { + return dto.map(this.dtoToCategory.bind(this)); + } else { + const ref = this; + const cat = { + get pages$(): Observable { + return ref.getPages(this.id); + } + }; + Object.keys(dto).forEach(key => cat[key] = dto[key]); + return cat as Category; + } + } + + private dtoToPage(dto: (PageDTO | PageDTO[])): (Page | Page[]) { + if (Array.isArray(dto)) { + return dto.map(this.dtoToPage.bind(this)); + } else { + const ref = this; + const page = { + get content$(): Observable { + return ref.getPageContent(this.category, this.id); + } + }; + Object.keys(dto).forEach(key => page[key] = dto[key]); + return page as Page; + } } } diff --git a/src/app/shared/services/event.service.spec.ts b/src/app/shared/services/event.service.spec.ts new file mode 100644 index 0000000..3e91180 --- /dev/null +++ b/src/app/shared/services/event.service.spec.ts @@ -0,0 +1,18 @@ +import { TestBed } from '@angular/core/testing'; + +import { EventService } from './event.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {AuthModule} from '../../auth/auth.module'; + +describe('EventService', () => { + let service: EventService; + + beforeEach(() => { + TestBed.configureTestingModule({imports: [HttpClientTestingModule, AuthModule]}); + service = TestBed.inject(EventService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/shared/services/event.service.ts b/src/app/shared/services/event.service.ts new file mode 100644 index 0000000..99e37aa --- /dev/null +++ b/src/app/shared/services/event.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@angular/core'; +import {Observable, of} from 'rxjs'; +import {HttpClient, HttpParams} from '@angular/common/http'; +import {Event, EventDTO} from '../../data/event'; +import {MediaService} from './media.service'; +import {AppConfig} from '../../core/config/app-config'; +import {map} from 'rxjs/operators'; +import {Media} from '../../data/media'; + +@Injectable({ + providedIn: 'root' +}) +export class EventService { + private _url: string; + + constructor(private _http: HttpClient, private _media: MediaService) { + this._url = AppConfig.INSTANCE.apiEndpoint; + } + + public getEvents(start?: Date, endOrN?: Date | number): Observable { + let opts: { params?: HttpParams; observe: 'body'; responseType: 'json' }; + if ((start === null || start === undefined) && (endOrN === null || endOrN === undefined)) { + opts = {observe: 'body', responseType: 'json'}; + } else { + const params = new HttpParams(); + if (start !== null && start !== undefined) { + params.set('start', start.toISOString()); + if (endOrN !== null && endOrN !== undefined) { + if (endOrN instanceof Date) { + params.set('end', endOrN.toISOString()); + } else { + params.set('next', endOrN.toString()); + } + } + } else if (endOrN !== null && endOrN !== undefined && !(endOrN instanceof Date)) { + params.set('next', endOrN.toString()); + } + opts = {params, observe: 'body', responseType: 'json'}; + } + return this._http.get(`${this._url}/event`, opts).pipe(map(this.dtoToEvent.bind(this))); + } + + private dtoToEvent(dto: (EventDTO | EventDTO[])): Event | Event[] { + if (Array.isArray(dto)) { + return dto.map(this.dtoToEvent.bind(this)); + } else { + const ref = this; + const event = { + get media$(): Observable { + if (this.media) { + return ref._media.getMedia(this.media); + } else { + return of(null); + } + } + }; + Object.keys(dto).forEach(key => event[key] = dto[key]); + return event as Event; + } + } +} diff --git a/src/app/shared/services/media.service.spec.ts b/src/app/shared/services/media.service.spec.ts new file mode 100644 index 0000000..ef8b93e --- /dev/null +++ b/src/app/shared/services/media.service.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from '@angular/core/testing'; + +import { MediaService } from './media.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; + +describe('MediaService', () => { + let service: MediaService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule] + }); + service = TestBed.inject(MediaService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/shared/services/media.service.ts b/src/app/shared/services/media.service.ts new file mode 100644 index 0000000..48d3020 --- /dev/null +++ b/src/app/shared/services/media.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import {Observable} from 'rxjs'; +import {Media} from '../../data/media'; +import {HttpClient} from '@angular/common/http'; +import {AppConfig} from '../../core/config/app-config'; + +@Injectable({ + providedIn: 'root' +}) +export class MediaService { + private readonly _url: string; + + constructor(private _http: HttpClient) { + this._url = AppConfig.INSTANCE.apiEndpoint; + } + + public getMedia(mediaId: string): Observable { + return this._http.get(`${this._url}/media/${encodeURIComponent(String(mediaId))}`); + } +} diff --git a/src/assets/config/config.dev.json b/src/assets/config/config.dev.json index 8e797bf..860bf12 100644 --- a/src/assets/config/config.dev.json +++ b/src/assets/config/config.dev.json @@ -1,3 +1,3 @@ { - "apiEndpoint": "http://localhost:5000/api" + "apiEndpoint": "https://minecraftschurli.ddns.net/pfarre-machstrasse/api" } diff --git a/src/index.html b/src/index.html index 87d74f0..5c03bb8 100644 --- a/src/index.html +++ b/src/index.html @@ -21,7 +21,6 @@ - - + diff --git a/src/styles.scss b/src/styles.scss index 0f5de21..6e0cb1b 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -2,6 +2,9 @@ @import '~bootstrap/scss/variables'; @import '~bootstrap/scss/mixins'; @import '~bootstrap/scss/utilities'; +@import '~bootstrap/scss/containers'; +@import '~bootstrap/scss/images'; +@import '~bootstrap/scss/utilities/api'; @import '~@angular/material/theming'; @import 'theme'; @@ -10,6 +13,10 @@ body { height: 100%; } +* { + box-sizing: border-box; +} + body { font-family: Roboto, 'Helvetica Neue', sans-serif; margin: 0; @@ -32,7 +39,11 @@ body { } .center-container { + align-self: center; flex-grow: 1; + max-width: 60rem; + overflow-y: auto; + width: 100%; } app-nav { diff --git a/src/theme.scss b/src/theme.scss index e7a3566..a08df58 100644 --- a/src/theme.scss +++ b/src/theme.scss @@ -1,8 +1,8 @@ @import '~@angular/material/theming'; $primary: mat-palette($mat-indigo); -$accent: mat-palette($mat-pink, A200, A100, A400); -$warn: mat-palette($mat-red); +$accent: mat-palette($mat-teal, A200, A100, A400); +$warn: mat-palette($mat-amber); $light-theme: mat-light-theme(( color: ( @@ -24,6 +24,9 @@ $dark-theme: mat-dark-theme(( $config: mat-get-color-config($config-or-theme); $primary: map-get($config, primary); $accent: map-get($config, accent); + $background: map-get($config, background); + $foreground: map-get($config, foreground); + $is-dark: map-get($config, is_dark); @include media-breakpoint-up(md) { .menu { @@ -54,12 +57,24 @@ $dark-theme: mat-dark-theme(( } } + body { + @if($is-dark) { + background-color: darken(map-get($background, background), 5%); + } @else { + background-color: darken(map-get($background, background), 10%); + } + } + + main.container { + background-color: map-get($background, background); + } + nav { background-color: mat-color($primary); - } - a { - color: mat-contrast($primary, 300); + a { + color: mat-contrast($primary, 300); + } } footer { From 4690128579b052f09f6eaac4509d0b5895abc1d2 Mon Sep 17 00:00:00 2001 From: Minecraftschurli Date: Thu, 6 May 2021 12:12:39 +0200 Subject: [PATCH 3/7] add admin interface menu manager Signed-off-by: Minecraftschurli --- .../features/admin/admin-routing.module.ts | 11 ++++++ src/app/features/admin/admin.component.html | 5 +++ src/app/features/admin/admin.component.scss | 8 ++++ .../features/admin/admin.component.spec.ts | 25 ++++++++++++ src/app/features/admin/admin.component.ts | 15 +++++++ src/app/features/admin/admin.module.ts | 26 +++++++++++++ .../menu-manager/menu-manager.component.html | 25 ++++++++++++ .../menu-manager/menu-manager.component.scss | 13 +++++++ .../menu-manager.component.spec.ts | 25 ++++++++++++ .../menu-manager/menu-manager.component.ts | 39 +++++++++++++++++++ 10 files changed, 192 insertions(+) create mode 100644 src/app/features/admin/admin-routing.module.ts create mode 100644 src/app/features/admin/admin.component.html create mode 100644 src/app/features/admin/admin.component.scss create mode 100644 src/app/features/admin/admin.component.spec.ts create mode 100644 src/app/features/admin/admin.component.ts create mode 100644 src/app/features/admin/admin.module.ts create mode 100644 src/app/features/admin/menu-manager/menu-manager.component.html create mode 100644 src/app/features/admin/menu-manager/menu-manager.component.scss create mode 100644 src/app/features/admin/menu-manager/menu-manager.component.spec.ts create mode 100644 src/app/features/admin/menu-manager/menu-manager.component.ts diff --git a/src/app/features/admin/admin-routing.module.ts b/src/app/features/admin/admin-routing.module.ts new file mode 100644 index 0000000..7e42028 --- /dev/null +++ b/src/app/features/admin/admin-routing.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AdminComponent } from './admin.component'; + +const routes: Routes = [{ path: '', component: AdminComponent }]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class AdminRoutingModule { } diff --git a/src/app/features/admin/admin.component.html b/src/app/features/admin/admin.component.html new file mode 100644 index 0000000..5601a83 --- /dev/null +++ b/src/app/features/admin/admin.component.html @@ -0,0 +1,5 @@ +
+ + + +
diff --git a/src/app/features/admin/admin.component.scss b/src/app/features/admin/admin.component.scss new file mode 100644 index 0000000..248abe2 --- /dev/null +++ b/src/app/features/admin/admin.component.scss @@ -0,0 +1,8 @@ +:host { + display: block; + height: 100%; +} + +.container { + height: 100%; +} diff --git a/src/app/features/admin/admin.component.spec.ts b/src/app/features/admin/admin.component.spec.ts new file mode 100644 index 0000000..eb28e42 --- /dev/null +++ b/src/app/features/admin/admin.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminComponent } from './admin.component'; + +describe('AdminComponent', () => { + let component: AdminComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AdminComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AdminComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/admin/admin.component.ts b/src/app/features/admin/admin.component.ts new file mode 100644 index 0000000..d267ab2 --- /dev/null +++ b/src/app/features/admin/admin.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-admin', + templateUrl: './admin.component.html', + styleUrls: ['./admin.component.scss'] +}) +export class AdminComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/src/app/features/admin/admin.module.ts b/src/app/features/admin/admin.module.ts new file mode 100644 index 0000000..bc0a9be --- /dev/null +++ b/src/app/features/admin/admin.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { AdminRoutingModule } from './admin-routing.module'; +import { AdminComponent } from './admin.component'; +import { MatTabsModule } from "@angular/material/tabs"; +import { MenuManagerComponent } from './menu-manager/menu-manager.component'; +import { DragDropModule } from "@angular/cdk/drag-drop"; +import { MatCardModule } from "@angular/material/card"; +import { SharedModule } from "../../shared/shared.module"; +import { MatListModule } from "@angular/material/list"; + + +@NgModule({ + declarations: [AdminComponent, MenuManagerComponent], + imports: [ + CommonModule, + AdminRoutingModule, + MatTabsModule, + DragDropModule, + MatCardModule, + SharedModule, + MatListModule + ] +}) +export class AdminModule { } diff --git a/src/app/features/admin/menu-manager/menu-manager.component.html b/src/app/features/admin/menu-manager/menu-manager.component.html new file mode 100644 index 0000000..53578c4 --- /dev/null +++ b/src/app/features/admin/menu-manager/menu-manager.component.html @@ -0,0 +1,25 @@ +
+ +
+ HI + + {{ cat.title }} + + + + + {{ page.title }} + + + + +
+
diff --git a/src/app/features/admin/menu-manager/menu-manager.component.scss b/src/app/features/admin/menu-manager/menu-manager.component.scss new file mode 100644 index 0000000..0627599 --- /dev/null +++ b/src/app/features/admin/menu-manager/menu-manager.component.scss @@ -0,0 +1,13 @@ +:host { + display: block; +} + +.dlg { + display: flex; + flex-flow: row wrap; + gap: 0.5rem; + + mat-card { + height: 100%; + } +} diff --git a/src/app/features/admin/menu-manager/menu-manager.component.spec.ts b/src/app/features/admin/menu-manager/menu-manager.component.spec.ts new file mode 100644 index 0000000..1ea620b --- /dev/null +++ b/src/app/features/admin/menu-manager/menu-manager.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MenuManagerComponent } from './menu-manager.component'; + +describe('MenuManagerComponent', () => { + let component: MenuManagerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MenuManagerComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MenuManagerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/admin/menu-manager/menu-manager.component.ts b/src/app/features/admin/menu-manager/menu-manager.component.ts new file mode 100644 index 0000000..23df57a --- /dev/null +++ b/src/app/features/admin/menu-manager/menu-manager.component.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; +import { Category } from '../../../data/category'; +import { ContentService } from '../../../shared/services/content.service'; +import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop'; +import { Page } from '../../../data/page'; +import { map, mergeMap } from 'rxjs/operators'; +import { Observable } from 'rxjs'; + +@Component({ + selector: 'app-menu-manager', + templateUrl: './menu-manager.component.html', + styleUrls: ['./menu-manager.component.scss'] +}) +export class MenuManagerComponent { + categories: Category[]; + pages: Page[][]; + + constructor(content: ContentService) { + content.getCategories().pipe(mergeMap((categories) => { + this.categories = categories; + this.pages = []; + return categories.map>((value, i) => value.pages$.pipe(map(pages => [pages, i]))); + }), mergeMap(value => value)).subscribe(([pages, i]) => this.pages[i] = pages); + } + + dropPage(event: CdkDragDrop): void { + console.log(event); + if (event.container === event.previousContainer) { + moveItemInArray(event.container.data, event.previousIndex, event.currentIndex); + } else { + transferArrayItem(event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex); + } + } + + dropCategory(event: CdkDragDrop): void { + console.log(event); + moveItemInArray(event.container.data, event.previousIndex, event.currentIndex); + } +} From 09f64f12a5c7e075e11dd0b67244a47d79edfe7b Mon Sep 17 00:00:00 2001 From: Minecraftschurli Date: Thu, 6 May 2021 12:18:30 +0200 Subject: [PATCH 4/7] fix lint errors Signed-off-by: Minecraftschurli --- src/app/features/admin/admin.module.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/features/admin/admin.module.ts b/src/app/features/admin/admin.module.ts index bc0a9be..156952d 100644 --- a/src/app/features/admin/admin.module.ts +++ b/src/app/features/admin/admin.module.ts @@ -3,12 +3,12 @@ import { CommonModule } from '@angular/common'; import { AdminRoutingModule } from './admin-routing.module'; import { AdminComponent } from './admin.component'; -import { MatTabsModule } from "@angular/material/tabs"; +import { MatTabsModule } from '@angular/material/tabs'; import { MenuManagerComponent } from './menu-manager/menu-manager.component'; -import { DragDropModule } from "@angular/cdk/drag-drop"; -import { MatCardModule } from "@angular/material/card"; -import { SharedModule } from "../../shared/shared.module"; -import { MatListModule } from "@angular/material/list"; +import { DragDropModule } from '@angular/cdk/drag-drop'; +import { MatCardModule } from '@angular/material/card'; +import { SharedModule } from '../../shared/shared.module'; +import { MatListModule } from '@angular/material/list'; @NgModule({ From f3d069f25eecb34ff16634465a77ef190b988bb3 Mon Sep 17 00:00:00 2001 From: Minecraftschurli Date: Thu, 6 May 2021 12:19:15 +0200 Subject: [PATCH 5/7] fix more lint errors Signed-off-by: Minecraftschurli --- src/app/features/admin/admin.component.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/app/features/admin/admin.component.ts b/src/app/features/admin/admin.component.ts index d267ab2..7aae543 100644 --- a/src/app/features/admin/admin.component.ts +++ b/src/app/features/admin/admin.component.ts @@ -1,15 +1,9 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; @Component({ selector: 'app-admin', templateUrl: './admin.component.html', styleUrls: ['./admin.component.scss'] }) -export class AdminComponent implements OnInit { - - constructor() { } - - ngOnInit(): void { - } - +export class AdminComponent { } From bbef54253bbdaf71b97dc365f972efd897a80a23 Mon Sep 17 00:00:00 2001 From: Georg Burkl Date: Mon, 10 May 2021 13:28:27 +0200 Subject: [PATCH 6/7] implement menu manager Signed-off-by: Minecraftschurli --- .idea/inspectionProfiles/Project_Default.xml | 85 ++++---------- src/app/app.component.spec.ts | 4 +- src/app/app.module.ts | 4 +- src/app/features/admin/admin.component.scss | 5 + src/app/features/admin/admin.module.ts | 16 ++- .../menu-manager/menu-manager.component.html | 94 +++++++++++---- .../menu-manager/menu-manager.component.scss | 39 ++++++- .../menu-manager.component.spec.ts | 3 + .../menu-manager/menu-manager.component.ts | 109 ++++++++++++++++-- src/main.ts | 2 +- src/test.ts | 2 +- src/utilities/index.ts | 2 + src/utilities/object.d.ts | 4 + src/utilities/object.spec.ts | 13 +++ src/utilities/object.ts | 66 +++++++++++ src/{ => utilities}/string.d.ts | 0 src/{ => utilities}/string.spec.ts | 0 src/{ => utilities}/string.ts | 8 +- 18 files changed, 348 insertions(+), 108 deletions(-) create mode 100644 src/utilities/index.ts create mode 100644 src/utilities/object.d.ts create mode 100644 src/utilities/object.spec.ts create mode 100644 src/utilities/object.ts rename src/{ => utilities}/string.d.ts (100%) rename src/{ => utilities}/string.spec.ts (100%) rename src/{ => utilities}/string.ts (96%) diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 2a0829d..03228a8 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -19,13 +19,11 @@