diff --git a/src/app/app.component.html b/src/app/app.component.html index dc33316..bc25e55 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,4 +1,6 @@ - - -

{{ test()|json }}

- + + +
+

{{title.getTitle()}}

+ +
\ No newline at end of file diff --git a/src/app/app.component.scss b/src/app/app.component.scss index e69de29..fa6ab73 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -0,0 +1,24 @@ +app-header { + width: 100%; +} + +main { + flex-grow: 1; + min-width: 100%; + width: fit-content; + box-sizing: border-box; + padding: 2rem; + + @media (min-width: 60rem) { + width: 80%; + min-width: 60rem; + max-width: 80rem; + } + + @media (max-width: 800px) { + width: 100%; + min-width: 100%; + max-width: 100%; + padding: 0.5rem; + } +} diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts deleted file mode 100644 index ae2557b..0000000 --- a/src/app/app.component.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { AppComponent } from './app.component'; - -describe('AppComponent', () => { - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [AppComponent], - }).compileComponents(); - }); - - it('should create the app', () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app).toBeTruthy(); - }); - - it('should have the \'Tower-Defence-Administration\' title', () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app.title).toEqual('Tower-Defence-Administration'); - }); - - it('should render title', () => { - const fixture = TestBed.createComponent(AppComponent); - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Hello, Tower-Defence-Administration'); - }); -}); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 3386adb..2b05453 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,29 +1,15 @@ -import { AsyncPipe, JsonPipe } from '@angular/common'; import { Component } from '@angular/core'; +import { Title } from '@angular/platform-browser'; import { RouterOutlet } from '@angular/router'; -import { AdminService } from '@core/server'; - -import { AuthService } from './core/auth/auth.service'; +import { HeaderComponent } from '@app/header/header.component'; +import { NotificationBoxComponent } from '@app/notification-box/notification-box.component'; @Component({ selector: 'app-root', - imports: [RouterOutlet, JsonPipe, AsyncPipe], + imports: [RouterOutlet, HeaderComponent, NotificationBoxComponent], templateUrl: './app.component.html', styleUrl: './app.component.scss' }) export class AppComponent { - constructor( - private api: AdminService, - private auth: AuthService - ){ - } - test() { - return this.api.configuration; - } - login(){ - this.auth.login(); - } - logout(){ - this.auth.logout(); - } + constructor(public title: Title) { } } diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 9fdd32e..dc2f322 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -25,6 +25,7 @@ export const appConfig: ApplicationConfig = { silentRenew: true, useRefreshToken: true, logLevel: LogLevel.Error, + } }), { provide: BASE_PATH, useValue: 'http://localhost:8080/api/v1' } diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index dc39edb..df958e6 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,3 +1,4 @@ import { Routes } from '@angular/router'; +import { DashboardComponent } from '@app/views/dashboard/dashboard.component'; -export const routes: Routes = []; +export const routes: Routes = [{ path: '', component: DashboardComponent, title: 'Home' }]; diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index c80c6df..65f8079 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { CanActivate, GuardResult, MaybeAsync, RedirectCommand, Router } from '@angular/router'; +import { CanActivate, GuardResult, MaybeAsync, Router } from '@angular/router'; import UserData from '@core/auth/UserData'; import { Configuration } from '@core/server'; import { OidcSecurityService } from 'angular-auth-oidc-client'; @@ -28,9 +28,13 @@ export class AuthService implements CanActivate { } canActivate(): MaybeAsync { - return new Observable((publish) => { + return new Observable(() => { this.oidcSecurityService.checkAuth().subscribe(({ isAuthenticated }) => { - publish.next(isAuthenticated ? true : new RedirectCommand(this.router.parseUrl('/'))); + if (isAuthenticated) { + return true; + } + this.login(); + return false; }); }); } diff --git a/src/app/core/notification/notification.service.ts b/src/app/core/notification/notification.service.ts new file mode 100644 index 0000000..995919a --- /dev/null +++ b/src/app/core/notification/notification.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core'; +import { Subject, Subscription } from 'rxjs'; + +export interface Notification { + msg: string; + type: NotificationType; +} + +export enum NotificationType { + Information = 'info', + Error = 'error', +} + +export type NotificationCallback = (notification: Notification) => void; + +@Injectable({ providedIn: 'root' }) +export class NotificationService { + private readonly bus: Subject = new Subject(); + private readonly subscribers: Map = new Map(); + + subscribe(subscriberId: string, callback: NotificationCallback): void { + this.subscribers.set( + subscriberId, + this.bus.subscribe({ + next: callback + }) + ); + } + + publish(msg: string, type: NotificationType = NotificationType.Information) { + this.bus.next({ msg, type }); + } + + unsubscribe(subscriberId: string) { + this.subscribers.get(subscriberId)?.unsubscribe(); + } +} diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html new file mode 100644 index 0000000..4b0df9a --- /dev/null +++ b/src/app/header/header.component.html @@ -0,0 +1,24 @@ + + + @if (auth.$user|async; as user) { + + {{ user.username|titlecase }} + + + } @else { + + } + diff --git a/src/app/header/header.component.scss b/src/app/header/header.component.scss new file mode 100644 index 0000000..e84d150 --- /dev/null +++ b/src/app/header/header.component.scss @@ -0,0 +1,45 @@ +.header { + margin: 0; + padding: 0.5rem 1rem; + z-index: 100; + height: fit-content; + background-color: var(--mat-sys-primary-container); + + nav { + display: flex; + height: fit-content; + width: 100%; + margin: 0; + box-sizing: border-box; + list-style: none; + gap: 0.25rem; + + @media (max-width: 800px) { + a:first-of-type { + display: none; + + } + } + } + + a, + button { + color: var(--mat-sys-primary); + + &:hover, + &.active { + background-color: var(--mat-sys-primary-overlay) + } + } + + &__login { + min-width: fit-content; + } + + @media (max-width: 500px) { + >a:last-of-type { + display: none; + + } + } +} diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts new file mode 100644 index 0000000..d9632de --- /dev/null +++ b/src/app/header/header.component.ts @@ -0,0 +1,49 @@ +import { AsyncPipe, TitleCasePipe } from '@angular/common'; +import { Component, OnInit } from '@angular/core'; +import { MatAnchor, MatButton, MatIconButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import { MatToolbar } from '@angular/material/toolbar'; +import { EventType, Router, RouterLink } from '@angular/router'; +import { AuthService } from '@core/auth/auth.service'; + +@Component({ + selector: 'app-header', + imports: [ + MatToolbar, + MatAnchor, + RouterLink, + MatIcon, + MatButton, + MatIconButton, + AsyncPipe, + TitleCasePipe + ], + templateUrl: './header.component.html', + styleUrl: './header.component.scss' +}) +export class HeaderComponent implements OnInit { + routes: Array<{ path: string, title: string, class: string }> = []; + + constructor( + private router: Router, + protected auth: AuthService + ) { + } + + ngOnInit(): void { + const routes = this.router.config + .filter(route => !route.path?.includes(':')) + .filter(route => !route.path?.includes('/')); + + this.router.events.subscribe((event) => { + if (event.type != EventType.NavigationEnd) { + return; + } + this.routes = routes.map(route => ({ + path: `/${route.path}`, + title: route.title as string, + class: `/${route.path}` == event.url ? 'active' : '' + })); + }); + } +} diff --git a/src/app/notification-box/notification-box.component.html b/src/app/notification-box/notification-box.component.html new file mode 100644 index 0000000..5dcc820 --- /dev/null +++ b/src/app/notification-box/notification-box.component.html @@ -0,0 +1,7 @@ +
+ @for (notification of notifications.values(); track notification){ + + {{notification.msg}} + + } +
diff --git a/src/app/notification-box/notification-box.component.scss b/src/app/notification-box/notification-box.component.scss new file mode 100644 index 0000000..5d9cfd1 --- /dev/null +++ b/src/app/notification-box/notification-box.component.scss @@ -0,0 +1,36 @@ +.notification-box { + position: absolute; + z-index: 100; + right: 0; + top: 3.5rem; // Header Height + + width: 15%; + min-width: 20rem; + + display: flex; + flex-direction: column; + padding: 0.5rem; + gap: 0.5rem; + + @media (max-width: 20rem) { + min-width: calc(100% - 0.5rem); + padding: 0.25rem; + } + + overflow: hidden; + + &__card { + width: 100%; + box-sizing: border-box; + + &.info { + color: var(--mat-sys-tertiary); + background-color: var(--mat-sys-tertiary-container); + } + + &.error { + color: var(--mat-sys-error); + background-color: var(--mat-sys-error-container); + } + } +} diff --git a/src/app/notification-box/notification-box.component.ts b/src/app/notification-box/notification-box.component.ts new file mode 100644 index 0000000..4d97c90 --- /dev/null +++ b/src/app/notification-box/notification-box.component.ts @@ -0,0 +1,41 @@ +import { animate, style, transition, trigger } from '@angular/animations'; +import { Component, OnInit } from '@angular/core'; +import { MatCardModule } from '@angular/material/card'; +import { Notification, NotificationService } from '@core/notification/notification.service'; + +const NOTIFICATION_TTL = 3000; + +@Component({ + selector: 'app-notification-box', + imports: [MatCardModule], + animations: [ + trigger('slideInOut', [ + transition(':enter', [ + style({ transform: 'translateX(100%)' }), + animate('200ms ease-in-out', style({ transform: 'translateX(0)' })), + ]), + transition(':leave', [animate('200ms ease-in-out', style({ transform: 'translateX(100%)' })),]) + ]), + ], + templateUrl: './notification-box.component.html', + styleUrl: './notification-box.component.scss' +}) +export class NotificationBoxComponent implements OnInit { + notifications: Map = new Map(); + + constructor(private notificationService: NotificationService) { + } + + ngOnInit(): void { + this.notificationService.subscribe('notification-box', this.onNotification.bind(this)); + } + + onNotification(notification: Notification): void { + const now = Date.now(); + this.notifications.set(now, notification); + + setTimeout(() => { + this.notifications.delete(now); + }, NOTIFICATION_TTL); + } +} diff --git a/src/app/views/dashboard/dashboard.component.html b/src/app/views/dashboard/dashboard.component.html new file mode 100644 index 0000000..e0c966a --- /dev/null +++ b/src/app/views/dashboard/dashboard.component.html @@ -0,0 +1,3 @@ +
+

Lorem ipsum dolor sit amet consectetur, adipisicing elit. Facere illo animi quidem repellat perspiciatis, excepturi amet corrupti ipsa sit consequuntur placeat ratione saepe velit asperiores suscipit esse quod minima exercitationem minus, alias laudantium inventore! Beatae cum nobis error suscipit cupiditate, praesentium itaque ut ipsa iusto in doloribus unde quisquam consequuntur.

+
diff --git a/src/app/views/dashboard/dashboard.component.scss b/src/app/views/dashboard/dashboard.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/views/dashboard/dashboard.component.ts b/src/app/views/dashboard/dashboard.component.ts new file mode 100644 index 0000000..4653ae7 --- /dev/null +++ b/src/app/views/dashboard/dashboard.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-dashboard', + imports: [], + templateUrl: './dashboard.component.html', + styleUrl: './dashboard.component.scss' +}) +export class DashboardComponent { + +} diff --git a/src/index.html b/src/index.html index 9f7922e..32aeb33 100644 --- a/src/index.html +++ b/src/index.html @@ -2,12 +2,14 @@ - TowerDefenceAdministration + Tower Defence - Administration + + - + - + \ No newline at end of file diff --git a/src/styles.scss b/src/styles.scss index 90d4ee0..e4f9c0d 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1 +1,82 @@ -/* You can add global styles to this file, and also import other style files */ +@use '@angular/material' as mat; + +html, +body { + height: 100%; + min-height: 100%; + margin: 0; + + background: var(--mat-sys-surface); + color: var(--mat-sys-on-surface); + + --mat-sys-primary-overlay: color-mix(in srgb, var(--mat-sys-primary) 10%, transparent); + + color-scheme: light dark; + font-family: var(--mat-sys-label-medium-font); + + @include mat.theme(( + color: mat.$azure-palette, + typography: Roboto, + density: 0 + )); +} + +app-root { + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; +} + +.mdc-button, +.mdc-fab { + &.abort, + &.error, + &.warn { + $ripple: var(color-mix(in srgb, var(--mat-sys-on-error) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent)); + $fg: var(--mat-sys-on-error); + $bg: var(--mat-sys-error); + $fg-container: var(--mat-sys-on-error-container); + $bg-container: var(--mat-sys-error-container); + + @include mat.button-overrides(( + // default + text-label-text-color: $bg, + text-ripple-color: $ripple, + text-state-layer-color: $bg, + // filled + filled-label-text-color: $fg, + filled-container-color: $bg, + filled-ripple-color: $ripple, + filled-state-layer-color: $fg, + )); + @include mat.fab-overrides(( + // default + container-color: $bg-container, + foreground-color: $fg-container, + state-layer-color: $fg-container, + ripple-color: $ripple, + // mini + small-container-color: $bg-container, + small-foreground-color: $fg-container, + small-state-layer-color: $fg-container, + small-ripple-color: $ripple + )); + } +} + +.mdc-fab.shadowless { + @include mat.fab-overrides(( + // default + container-elevation-shadow: transparent, + hover-container-elevation-shadow: transparent, + pressed-container-elevation-shadow: transparent, + focus-container-elevation-shadow: transparent, + // mini + small-container-elevation-shadow: transparent, + small-hover-container-elevation-shadow: transparent, + small-pressed-container-elevation-shadow: transparent, + small-focus-container-elevation-shadow: transparent, + )); +} \ No newline at end of file