feat: integrate Tailwind CSS and implement dark mode functionality

- Added Tailwind CSS dependencies and configured PostCSS.
- Updated app component to wrap router outlet with a styled div for dark mode support.
- Modified routing to include a default route and added auth guards.
- Implemented dark mode toggle functionality in content component.
- Enhanced login component with improved styling and lifecycle management.
- Created items component with basic structure and routing.
- Added global styles for body and dark mode variants.
- Updated tests for new items component.
This commit is contained in:
Marek Lesko
2025-08-06 19:53:23 +00:00
parent 02116aa3df
commit 7919a93ab4
17 changed files with 890 additions and 52 deletions

View File

@@ -1 +1,3 @@
<router-outlet />
<div class="dark:bg-amber-950 bg-amber-50 text-amber-950 dark:text-amber-50 flex flex-col grow">
<router-outlet />
</div>

View File

@@ -1,20 +1,27 @@
import { Routes } from '@angular/router';
import { Login } from './login/login';
import { authGuard } from './app.route.guard';
import { App } from './app';
export const routes: Routes = [
{
path: '',
component: App
},
{
path: 'login',
component: Login,
canActivate: [authGuard]
},
{
path: 'content',
loadComponent: () => import('./content/content').then(m => m.Content),
canActivate: [authGuard],
children: [
{
path: '',
loadComponent: () => import('./content/content').then(m => m.Content),
path: 'items',
loadComponent: () => import('./content/items/items').then(m => m.Items),
canActivate: [authGuard],
}
],
}]
}
];

View File

@@ -0,0 +1,5 @@
:host {
display: flex;
flex-direction: column;
flex: 1;
}

View File

@@ -1,15 +1,13 @@
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { APP_INITIALIZER, Component, inject, OnInit, provideAppInitializer } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router';
import { DefaultOAuthInterceptor, OAuthService } from 'angular-oauth2-oidc';
import { AppConfigService } from './services/config.service';
import { OAuthService } from 'angular-oauth2-oidc';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
providers: [
OAuthService,
],
],
templateUrl: './app.html',
styleUrl: './app.scss'
})
@@ -26,6 +24,22 @@ export class App implements OnInit {
});
}
ngOnInit(): void {
this.as.loadDiscoveryDocumentAndLogin().then(() => this.router.navigate(['login']));
if (!sessionStorage.getItem('isDarkMode')) {
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'true' : 'false';
sessionStorage.setItem('isDarkMode', isDarkMode);
setTheme();
}
this.as.loadDiscoveryDocumentAndLogin()
.then(() => this.router.navigate(['login']));
}
}
export function setTheme(): void {
const isDarkMode = sessionStorage.getItem('isDarkMode');
if (isDarkMode === 'true') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}

View File

@@ -1 +1,8 @@
<span>{{data()|json}}</span>
<span>{{data()|json}}</span>
<router-outlet />
<div>
<a [routerLink]="['/']">Back to Home</a>
</div>
<div>
<a (click)="toggleMode()" class="cursor-pointer">Change mode</a>
</div>

View File

@@ -1,16 +1,18 @@
import { JsonPipe } from '@angular/common';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Component, isDevMode, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Component, OnInit, signal } from '@angular/core';
import { OAuthService } from 'angular-oauth2-oidc';
import { AppConfigService } from '../services/config.service';
import { RouterLink, RouterOutlet } from "@angular/router";
import { setTheme } from '../app';
@Component({
selector: 'app-content',
imports: [JsonPipe],
imports: [JsonPipe, RouterOutlet, RouterLink],
templateUrl: './content.html',
styleUrl: './content.scss'
})
export class Content {
export class Content implements OnInit {
data = signal({});
constructor(httpClient: HttpClient, readonly as: OAuthService, readonly cs: AppConfigService) {
@@ -19,4 +21,16 @@ export class Content {
this.data.set(data);
});
}
ngOnInit(): void {
setTheme();
}
toggleMode() {
sessionStorage.getItem('isDarkMode') === 'true' ?
sessionStorage.setItem('isDarkMode', 'false') : sessionStorage.setItem('isDarkMode', 'true');
setTheme();
}
}

View File

@@ -0,0 +1,4 @@
<p>items works!</p>
<div>
<a [routerLink]="['/','content']">Back to content</a>
</div>

View File

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Items } from './items';
describe('Items', () => {
let component: Items;
let fixture: ComponentFixture<Items>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Items]
})
.compileComponents();
fixture = TestBed.createComponent(Items);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-items',
imports: [RouterLink],
templateUrl: './items.html',
styleUrl: './items.scss'
})
export class Items {
}

View File

@@ -1,2 +1 @@
Logging in...
<div class="flex grow flex-col justify-center text-center text-xl">Logging in...</div>

View File

@@ -0,0 +1,6 @@
:host {
display: flex;
flex-direction: column;
flex: 1;
}

View File

@@ -1,13 +1,16 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { OAuthService } from 'angular-oauth2-oidc';
import { Subject, takeUntil } from 'rxjs';
@Component({
selector: 'app-login',
templateUrl: './login.html',
styleUrl: './login.scss'
})
export class Login implements OnInit {
export class Login implements OnInit, OnDestroy {
private readonly _destroy: Subject<void> = new Subject<void>();
constructor(readonly as: OAuthService, readonly router: Router) { }
@@ -15,19 +18,27 @@ export class Login implements OnInit {
if (this.as.hasValidAccessToken() && this.as.hasValidIdToken()) {
this.getUserInfo();
} else {
this.as.events.subscribe(event => {
if (event.type === 'token_received')
if (this.as.hasValidIdToken()) {
this.getUserInfo();
}
})
this.as.events
.pipe(
takeUntil(this._destroy))
.subscribe(event => {
if (event.type === 'token_received')
if (this.as.hasValidIdToken()) {
this.getUserInfo();
}
});
}
}
ngOnDestroy(): void {
this._destroy.next();
this._destroy.complete();
}
getUserInfo(): void {
this.as.loadUserProfile().then(value => {
console.log('User profile loaded:', value);
this.router.navigate(['/content']);
});
this.as.loadUserProfile()
.then(_ => {
this.router.navigate(['content', 'items']);
})
.catch(_ => this.router.navigate(['content', 'items']));
}
}