CHORE: setup
All checks were successful
Quality Check / Linting (push) Successful in 24s
Build Application / build (push) Successful in 55s
Build Application / build-docker (push) Successful in 8s
Build Application / release (push) Successful in 4s

This commit is contained in:
Snoweuph 2025-02-02 19:59:34 +01:00
parent 2c14a7dd7a
commit a4f27ed881
Signed by: snoweuph
GPG key ID: BEFC41DA223CEC55
19 changed files with 385 additions and 61 deletions

View file

@ -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>

View file

@ -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;
}
}

View file

@ -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');
});
});

View file

@ -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) { }
}

View file

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

View file

@ -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' }];

View file

@ -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;
});
});
}

View 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();
}
}

View 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>

View 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;
}
}
}

View 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' : ''
}));
});
}
}

View 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>

View 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);
}
}
}

View 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);
}
}

View 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>

View 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 {
}

View file

@ -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>

View file

@ -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,
));
}