Compare commits
1 commit
v0.0.0-rc.
...
trunk
Author | SHA1 | Date | |
---|---|---|---|
a4f27ed881 |
19 changed files with 385 additions and 61 deletions
|
@ -1,4 +1,6 @@
|
|||
<button (click)="login()">PAIN</button>
|
||||
<button (click)="logout()">ASS</button>
|
||||
<p>{{ test()|json }}</p>
|
||||
<app-header></app-header>
|
||||
<app-notification-box></app-notification-box>
|
||||
<main>
|
||||
<h1>{{title.getTitle()}}</h1>
|
||||
<router-outlet />
|
||||
</main>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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) { }
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ export const appConfig: ApplicationConfig = {
|
|||
silentRenew: true,
|
||||
useRefreshToken: true,
|
||||
logLevel: LogLevel.Error,
|
||||
|
||||
}
|
||||
}),
|
||||
{ provide: BASE_PATH, useValue: 'http://localhost:8080/api/v1' }
|
||||
|
|
|
@ -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' }];
|
||||
|
|
|
@ -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<GuardResult> {
|
||||
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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
37
src/app/core/notification/notification.service.ts
Normal file
37
src/app/core/notification/notification.service.ts
Normal file
|
@ -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<Notification> = new Subject();
|
||||
private readonly subscribers: Map<string, Subscription> = new Map<string, Subscription>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
24
src/app/header/header.component.html
Normal file
24
src/app/header/header.component.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<mat-toolbar class="header">
|
||||
<nav>
|
||||
<a routerLink="" mat-button>
|
||||
<mat-icon>domain</mat-icon>
|
||||
Tower Defence Administration
|
||||
</a>
|
||||
@for (route of routes; track route) {
|
||||
<a mat-button routerLink="{{ route.path }}" class="{{ route.class }}">{{ route.title }}</a>
|
||||
}
|
||||
</nav>
|
||||
@if (auth.$user|async; as user) {
|
||||
<a mat-button href="https://keycloak.szut.dev/auth/realms/szut/account">
|
||||
{{ user.username|titlecase }}
|
||||
</a>
|
||||
<button mat-icon-button (click)="auth.logout()">
|
||||
<mat-icon>logout</mat-icon>
|
||||
</button>
|
||||
} @else {
|
||||
<button mat-button class="header__login" (click)="auth.login()">
|
||||
<mat-icon>login</mat-icon>
|
||||
Login
|
||||
</button>
|
||||
}
|
||||
</mat-toolbar>
|
45
src/app/header/header.component.scss
Normal file
45
src/app/header/header.component.scss
Normal file
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
49
src/app/header/header.component.ts
Normal file
49
src/app/header/header.component.ts
Normal file
|
@ -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' : ''
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
7
src/app/notification-box/notification-box.component.html
Normal file
7
src/app/notification-box/notification-box.component.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<div class="notification-box">
|
||||
@for (notification of notifications.values(); track notification){
|
||||
<mat-card class="notification-box__card {{notification.type}}" [@slideInOut]>
|
||||
<mat-card-content>{{notification.msg}}</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
36
src/app/notification-box/notification-box.component.scss
Normal file
36
src/app/notification-box/notification-box.component.scss
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
41
src/app/notification-box/notification-box.component.ts
Normal file
41
src/app/notification-box/notification-box.component.ts
Normal file
|
@ -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<number, Notification> = 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);
|
||||
}
|
||||
}
|
3
src/app/views/dashboard/dashboard.component.html
Normal file
3
src/app/views/dashboard/dashboard.component.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<div class="dashboard">
|
||||
<p>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.</p>
|
||||
</div>
|
0
src/app/views/dashboard/dashboard.component.scss
Normal file
0
src/app/views/dashboard/dashboard.component.scss
Normal file
11
src/app/views/dashboard/dashboard.component.ts
Normal file
11
src/app/views/dashboard/dashboard.component.ts
Normal file
|
@ -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 {
|
||||
|
||||
}
|
|
@ -2,12 +2,14 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>TowerDefenceAdministration</title>
|
||||
<title>Tower Defence - Administration</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<body class="mat-typography">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
|
@ -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,
|
||||
));
|
||||
}
|
Loading…
Add table
Reference in a new issue