Compare commits

..

No commits in common. "trunk" and "v0.0.0-rc.1" have entirely different histories.

19 changed files with 61 additions and 385 deletions

View file

@ -1,6 +1,4 @@
<app-header></app-header>
<app-notification-box></app-notification-box>
<main>
<h1>{{title.getTitle()}}</h1>
<router-outlet />
</main>
<button (click)="login()">PAIN</button>
<button (click)="logout()">ASS</button>
<p>{{ test()|json }}</p>
<router-outlet />

View file

@ -1,24 +0,0 @@
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;
}
}

View file

@ -0,0 +1,30 @@
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');
});
});

View file

@ -1,15 +1,29 @@
import { AsyncPipe, JsonPipe } from '@angular/common';
import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { RouterOutlet } from '@angular/router';
import { HeaderComponent } from '@app/header/header.component';
import { NotificationBoxComponent } from '@app/notification-box/notification-box.component';
import { AdminService } from '@core/server';
import { AuthService } from './core/auth/auth.service';
@Component({
selector: 'app-root',
imports: [RouterOutlet, HeaderComponent, NotificationBoxComponent],
imports: [RouterOutlet, JsonPipe, AsyncPipe],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
constructor(public title: Title) { }
constructor(
private api: AdminService,
private auth: AuthService
){
}
test() {
return this.api.configuration;
}
login(){
this.auth.login();
}
logout(){
this.auth.logout();
}
}

View file

@ -25,7 +25,6 @@ export const appConfig: ApplicationConfig = {
silentRenew: true,
useRefreshToken: true,
logLevel: LogLevel.Error,
}
}),
{ provide: BASE_PATH, useValue: 'http://localhost:8080/api/v1' }

View file

@ -1,4 +1,3 @@
import { Routes } from '@angular/router';
import { DashboardComponent } from '@app/views/dashboard/dashboard.component';
export const routes: Routes = [{ path: '', component: DashboardComponent, title: 'Home' }];
export const routes: Routes = [];

View file

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { CanActivate, GuardResult, MaybeAsync, Router } from '@angular/router';
import { CanActivate, GuardResult, MaybeAsync, RedirectCommand, Router } from '@angular/router';
import UserData from '@core/auth/UserData';
import { Configuration } from '@core/server';
import { OidcSecurityService } from 'angular-auth-oidc-client';
@ -28,13 +28,9 @@ export class AuthService implements CanActivate {
}
canActivate(): MaybeAsync<GuardResult> {
return new Observable(() => {
return new Observable((publish) => {
this.oidcSecurityService.checkAuth().subscribe(({ isAuthenticated }) => {
if (isAuthenticated) {
return true;
}
this.login();
return false;
publish.next(isAuthenticated ? true : new RedirectCommand(this.router.parseUrl('/')));
});
});
}

View file

@ -1,37 +0,0 @@
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();
}
}

View file

@ -1,24 +0,0 @@
<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>

View file

@ -1,45 +0,0 @@
.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;
}
}
}

View file

@ -1,49 +0,0 @@
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' : ''
}));
});
}
}

View file

@ -1,7 +0,0 @@
<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>

View file

@ -1,36 +0,0 @@
.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);
}
}
}

View file

@ -1,41 +0,0 @@
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);
}
}

View file

@ -1,3 +0,0 @@
<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>

View file

@ -1,11 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-dashboard',
imports: [],
templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.scss'
})
export class DashboardComponent {
}

View file

@ -2,14 +2,12 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tower Defence - Administration</title>
<title>TowerDefenceAdministration</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 class="mat-typography">
<body>
<app-root></app-root>
</body>
</html>
</html>

View file

@ -1,82 +1 @@
@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,
));
}
/* You can add global styles to this file, and also import other style files */