Basic Dashboard #14

Merged
igorpropisnov merged 7 commits from feature/dashboard-basics into main 2024-07-18 18:39:49 +02:00
8 changed files with 307 additions and 314 deletions
Showing only changes of commit 09cc4cf0e7 - Show all commits

View File

@ -1,36 +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 "frontend" title', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('frontend');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain(
'Hello, frontend'
);
});
});

View File

@ -1,16 +1,14 @@
import { Component } from '@angular/core'; import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { ThemeService } from './shared/service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
providers: [], providers: [],
imports: [RouterOutlet], imports: [RouterOutlet, CommonModule],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.scss', styleUrl: './app.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AppComponent { export class AppComponent {}
public constructor(private readonly themeService: ThemeService) {}
}

View File

@ -1,15 +1,8 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
const publicRoutes: Routes = [ const simpleLayoutRoutes: Routes = [
{ {
path: '', path: '',
loadComponent: () =>
import('./pages/home-root/home-root.component').then(
(m) => m.HomeComponent
),
},
{
path: 'welcome',
loadComponent: () => loadComponent: () =>
import('./pages/register-root/register-root.component').then( import('./pages/register-root/register-root.component').then(
(m) => m.RegisterRootComponent (m) => m.RegisterRootComponent
@ -36,12 +29,29 @@ const protectedRoutes: Routes = [
]; ];
export const routes: Routes = [ export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./layout/simple-layout/simple-layout.component').then(
(m) => m.LayoutSimpleComponent
),
children: simpleLayoutRoutes,
},
{
path: '',
loadComponent: () =>
import('./layout/main-layout/layout.component').then(
(m) => m.LayoutComponent
),
children: [
{ {
path: '', path: '',
children: [ children: [
...publicRoutes,
...protectedRoutes, ...protectedRoutes,
{ path: '', redirectTo: '', pathMatch: 'full' }, { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
], ],
}, },
],
},
{ path: '**', redirectTo: '' },
]; ];

View File

@ -0,0 +1,127 @@
<div class="flex h-screen overflow-hidden">
<!-- Sidebar -->
<div
[ngStyle]="navigation"
[class]="
isCollapsed
? 'bg-primary w-0 md:w-20 transition-all duration-300 ease-in-out'
: showMobileMenu
? 'bg-primary w-64 transition-all duration-300 ease-in-out'
: isDesktopCollapsed
? 'bg-primary w-48 md:w-16 transition-all duration-300 ease-in-out'
: 'bg-primary w-48 md:w-48 transition-all duration-300 ease-in-out'
"
class="transform h-full z-20 overflow-y-auto fixed md:relative">
<div
[ngClass]="showMobileMenu ? 'justify-center' : 'justify-between'"
[ngStyle]="navigation"
class="p-1 w-full h-16 bg-base-100 flex items-center relative">
<div class="flex items-center justify-center h-full w-full">
<div class="flex items-center space-x-4">
@if (!isCollapsed && !isDesktopCollapsed) {
<div class="text-primary">Logo</div>
}
@if (!isCollapsed && !showMobileMenu) {
<button
(click)="toggleDesktopSidebar()"
class="flex items-center justify-center w-10 h-10 rounded-full">
@if (isDesktopCollapsed) {
<svg
class="stroke-current text-primary w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="3">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
} @else {
<svg
class="stroke-current text-primary w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="3">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 19.5 8.25 12l7.5-7.5" />
</svg>
}
</button>
}
</div>
</div>
</div>
<ul class="m-1">
<li
class="cursor-pointer rounded-btn"
[ngClass]="{
'bg-base-100 text-primary': item.active,
'text-primary-content hover:text-base-content': !item.active
}"
(click)="setActive(item)"
*ngFor="let item of menuItems">
<div
class="flex justify-center p-2"
*ngIf="isDesktopCollapsed && !showMobileMenu">
<span class="p-1" [innerHTML]="item.icon"></span>
</div>
<div
class="flex items-center rounded-btn justify-between cursor-pointer px-1 py-2"
*ngIf="!isDesktopCollapsed || showMobileMenu">
<div class="flex items-center">
<span [innerHTML]="item.icon" class="mx-2"></span>
<span>{{ item.name }}</span>
</div>
</div>
</li>
</ul>
</div>
<div class="flex flex-col flex-grow">
<header
[ngStyle]="navigation"
class="p-4 bg-primary text-primary-content flex items-center h-16">
<div class="w-10 flex items-center justify-center md:hidden">
<label class="btn btn-ghost swap swap-rotate">
<input
type="checkbox"
(change)="toggleSidebar()"
[checked]="!isCollapsed" />
<svg
class="swap-off fill-current"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 512 512">
<path
d="M64,384H448V341.33H64Zm0-106.67H448V234.67H64ZM64,128v42.67H448V128Z" />
</svg>
<svg
class="swap-on fill-current"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 512 512">
<polygon
points="400 145.49 366.51 112 256 222.51 145.49 112 112 145.49 222.51 256 112 366.51 145.49 400 256 289.49 366.51 400 400 366.51 289.49 256 400 145.49" />
</svg>
</label>
</div>
</header>
<div [ngStyle]="mainContent" class="px-8 py-4 flex-grow text-2xl p-4">
<router-outlet></router-outlet>
</div>
</div>
<div
*ngIf="!isCollapsed"
class="fixed inset-0 bg-black bg-opacity-50 z-10 md:hidden"
(click)="toggleSidebar()"></div>
</div>

View File

@ -0,0 +1,139 @@
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
HostListener,
OnInit,
} from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { Router, RouterOutlet } from '@angular/router';
import { BackgroundPatternService, ThemeService } from '../../shared/service';
interface MenuItem {
name: string;
icon: SafeHtml;
route: string;
active?: boolean;
}
@Component({
selector: 'app-layout',
standalone: true,
providers: [],
imports: [RouterOutlet, CommonModule],
templateUrl: './layout.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LayoutComponent implements OnInit {
public isCollapsed: boolean = false;
public isDesktopCollapsed: boolean = false;
public showMobileMenu: boolean = false;
public userHasInteracted: boolean = false;
public menuItems: MenuItem[] = [
{
name: 'Dashboard',
route: '/dashboard',
icon: this.sanitizer
.bypassSecurityTrustHtml(`<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
</svg>`),
},
];
public mainContent: { 'background-image': string } | null = null;
public navigation: { 'background-image': string } | null = null;
public constructor(
private readonly sanitizer: DomSanitizer,
private readonly router: Router,
private readonly backgroundPatternService: BackgroundPatternService,
private readonly themeService: ThemeService,
private readonly el: ElementRef
) {}
public ngOnInit(): void {
this.setActiveItemBasedOnRoute();
this.router.events.subscribe(() => {
this.setActiveItemBasedOnRoute();
});
this.setBackground();
this.onResize();
}
@HostListener('window:resize', ['$event'])
public onResize(): void {
if (window.innerWidth >= 768) {
this.showMobileMenu = false;
this.isCollapsed = false;
} else {
this.isDesktopCollapsed = false;
this.isCollapsed = true;
this.showMobileMenu = false;
}
}
public setBackground(): void {
const theme = this.themeService.getTheme();
let opacity: number;
if (theme === 'dark') {
opacity = 0.05;
} else {
opacity = 0.1;
}
const colorPrimary = getComputedStyle(
this.el.nativeElement
).getPropertyValue('--p');
const colorPrimaryC = getComputedStyle(
this.el.nativeElement
).getPropertyValue('--pc');
const svgUrlMainContent = this.backgroundPatternService.getPlusPattern(
colorPrimary,
opacity
);
const svgUrlNavigation = this.backgroundPatternService.getBankNotePattern(
colorPrimaryC,
opacity
);
this.mainContent = {
'background-image': `url("${svgUrlMainContent}")`,
};
this.navigation = {
'background-image': `url("${svgUrlNavigation}")`,
};
}
public toggleSidebar(): void {
if (window.innerWidth < 768) {
this.showMobileMenu = !this.showMobileMenu;
this.isCollapsed = !this.showMobileMenu;
} else {
this.isDesktopCollapsed = !this.isDesktopCollapsed;
}
this.userHasInteracted = true;
}
public toggleDesktopSidebar(): void {
this.isDesktopCollapsed = !this.isDesktopCollapsed;
}
public setActive(item: MenuItem): void {
this.menuItems.forEach((menu: MenuItem) => {
menu.active = false;
});
item.active = true;
}
private setActiveItemBasedOnRoute(): void {
const url = this.router.url;
this.menuItems.forEach((item: MenuItem) => {
item.active = url.startsWith(item.route);
});
}
}

View File

@ -0,0 +1,12 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-simple-layout',
standalone: true,
imports: [RouterOutlet],
template: `
<router-outlet></router-outlet>
`,
})
export class LayoutSimpleComponent {}

View File

@ -1,129 +1 @@
<div class="flex h-screen overflow-hidden"> <h1>Dashboard Works</h1>
<!-- Sidebar -->
<div
[ngStyle]="navigation"
[class]="
isCollapsed
? 'bg-primary w-0 md:w-20 transition-all duration-300 ease-in-out'
: showMobileMenu
? 'bg-primary w-64 transition-all duration-300 ease-in-out'
: isDesktopCollapsed
? 'bg-primary w-48 md:w-16 transition-all duration-300 ease-in-out'
: 'bg-primary w-48 md:w-48 transition-all duration-300 ease-in-out'
"
class="transform h-full z-20 overflow-y-auto fixed md:relative">
<div
[ngClass]="showMobileMenu ? 'justify-center' : 'justify-between'"
[ngStyle]="navigation"
class="p-1 w-full h-16 bg-base-100 flex items-center relative">
<div class="flex items-center justify-center h-full w-full">
<div class="flex items-center space-x-4">
@if (!isCollapsed && !isDesktopCollapsed) {
<div class="text-primary">Logo</div>
}
@if (!isCollapsed && !showMobileMenu) {
<button
(click)="toggleDesktopSidebar()"
class="flex items-center justify-center w-10 h-10 rounded-full">
@if (isDesktopCollapsed) {
<svg
class="stroke-current text-primary w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="3">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
} @else {
<svg
class="stroke-current text-primary w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="3">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 19.5 8.25 12l7.5-7.5" />
</svg>
}
</button>
}
</div>
</div>
</div>
<ul class="m-1">
<li
class="cursor-pointer rounded-btn"
[ngClass]="{
'bg-base-100 text-primary': item.active,
'text-primary-content hover:text-base-content': !item.active
}"
(click)="setActive(item)"
*ngFor="let item of menuItems">
<div
class="flex justify-center p-2"
*ngIf="isDesktopCollapsed && !showMobileMenu">
<span class="p-1" [innerHTML]="item.icon"></span>
</div>
<div
class="flex items-center rounded-btn justify-between cursor-pointer px-1 py-2"
*ngIf="!isDesktopCollapsed || showMobileMenu">
<div class="flex items-center">
<span [innerHTML]="item.icon" class="mx-2"></span>
<span>{{ item.name }}</span>
</div>
</div>
</li>
</ul>
</div>
<div class="flex flex-col flex-grow">
<header
[ngStyle]="navigation"
class="p-4 bg-primary text-primary-content flex items-center h-16">
<div class="w-10 flex items-center justify-center md:hidden">
<label class="btn btn-ghost swap swap-rotate">
<input
type="checkbox"
(change)="toggleSidebar()"
[checked]="!isCollapsed" />
<svg
class="swap-off fill-current"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 512 512">
<path
d="M64,384H448V341.33H64Zm0-106.67H448V234.67H64ZM64,128v42.67H448V128Z" />
</svg>
<svg
class="swap-on fill-current"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 512 512">
<polygon
points="400 145.49 366.51 112 256 222.51 145.49 112 112 145.49 222.51 256 112 366.51 145.49 400 256 289.49 366.51 400 400 366.51 289.49 256 400 145.49" />
</svg>
</label>
</div>
</header>
<div [ngStyle]="mainContent" class="px-8 py-4 flex-grow text-2xl p-4">
<p>isCollapsed: {{ isCollapsed }}</p>
<p>showMobileMenu: {{ showMobileMenu }}</p>
<p>isDesktopCollapsed: {{ isDesktopCollapsed }}</p>
</div>
</div>
<div
*ngIf="!isCollapsed"
class="fixed inset-0 bg-black bg-opacity-50 z-10 md:hidden"
(click)="toggleSidebar()"></div>
</div>

View File

@ -1,22 +1,5 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import { ChangeDetectionStrategy, Component } from '@angular/core';
ChangeDetectionStrategy,
Component,
ElementRef,
HostListener,
OnInit,
} from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { BackgroundPatternService, ThemeService } from '../../shared/service';
interface MenuItem {
name: string;
icon: SafeHtml;
route: string;
active?: boolean;
}
@Component({ @Component({
selector: 'app-dashboard-root', selector: 'app-dashboard-root',
@ -27,116 +10,4 @@ interface MenuItem {
styleUrl: './dashboard-root.component.scss', styleUrl: './dashboard-root.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class DashboardRootComponent implements OnInit { export class DashboardRootComponent {}
public isCollapsed: boolean = false;
public isDesktopCollapsed: boolean = false;
public showMobileMenu: boolean = false;
public userHasInteracted: boolean = false;
public menuItems: MenuItem[] = [
{
name: 'Dashboard',
route: '/dashboard',
icon: this.sanitizer
.bypassSecurityTrustHtml(`<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
</svg>`),
},
];
public mainContent: { 'background-image': string } | null = null;
public navigation: { 'background-image': string } | null = null;
public constructor(
private readonly sanitizer: DomSanitizer,
private readonly router: Router,
private readonly backgroundPatternService: BackgroundPatternService,
private readonly themeService: ThemeService,
private readonly el: ElementRef
) {}
public ngOnInit(): void {
this.setActiveItemBasedOnRoute();
this.router.events.subscribe(() => {
this.setActiveItemBasedOnRoute();
});
this.setBackground();
this.onResize();
}
@HostListener('window:resize', ['$event'])
public onResize(): void {
if (window.innerWidth >= 768) {
this.showMobileMenu = false;
this.isCollapsed = false;
} else {
this.isDesktopCollapsed = false;
this.isCollapsed = true;
this.showMobileMenu = false;
}
}
public setBackground(): void {
const theme = this.themeService.getTheme();
let opacity: number;
if (theme === 'dark') {
opacity = 0.05;
} else {
opacity = 0.1;
}
const colorPrimary = getComputedStyle(
this.el.nativeElement
).getPropertyValue('--p');
const colorPrimaryC = getComputedStyle(
this.el.nativeElement
).getPropertyValue('--pc');
const svgUrlMainContent = this.backgroundPatternService.getPlusPattern(
colorPrimary,
opacity
);
const svgUrlNavigation = this.backgroundPatternService.getBankNotePattern(
colorPrimaryC,
opacity
);
console.log(this.mainContent, this.navigation);
this.mainContent = {
'background-image': `url("${svgUrlMainContent}")`,
};
this.navigation = {
'background-image': `url("${svgUrlNavigation}")`,
};
}
public toggleSidebar(): void {
if (window.innerWidth < 768) {
this.showMobileMenu = !this.showMobileMenu;
this.isCollapsed = !this.showMobileMenu;
} else {
this.isDesktopCollapsed = !this.isDesktopCollapsed;
}
this.userHasInteracted = true;
}
public toggleDesktopSidebar(): void {
this.isDesktopCollapsed = !this.isDesktopCollapsed;
}
public setActive(item: MenuItem): void {
this.menuItems.forEach((menu: MenuItem) => {
menu.active = false;
});
item.active = true;
}
private setActiveItemBasedOnRoute(): void {
const url = this.router.url;
this.menuItems.forEach((item: MenuItem) => {
item.active = url.startsWith(item.route);
});
}
}