Compare commits
No commits in common. "trunk" and "v0.0.0-rc.1" have entirely different histories.
trunk
...
v0.0.0-rc.
19 changed files with 61 additions and 385 deletions
|
@ -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 />
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
30
src/app/app.component.spec.ts
Normal file
30
src/app/app.component.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ export const appConfig: ApplicationConfig = {
|
|||
silentRenew: true,
|
||||
useRefreshToken: true,
|
||||
logLevel: LogLevel.Error,
|
||||
|
||||
}
|
||||
}),
|
||||
{ provide: BASE_PATH, useValue: 'http://localhost:8080/api/v1' }
|
||||
|
|
|
@ -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 = [];
|
||||
|
|
|
@ -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('/')));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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' : ''
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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 */
|
||||
|
|
Loading…
Add table
Reference in a new issue