style employee list (#6)
Co-authored-by: Phan Huy Tran <p.tran@neusta.de> Reviewed-on: #6 Co-authored-by: Jan-Marlon Leibl <jleibl@proton.me> Co-committed-by: Jan-Marlon Leibl <jleibl@proton.me>
This commit is contained in:
parent
da378e7555
commit
53c0fde21f
@ -23,6 +23,7 @@
|
|||||||
"keycloak-angular": "^16.1.0",
|
"keycloak-angular": "^16.1.0",
|
||||||
"rxjs": "~7.8.1",
|
"rxjs": "~7.8.1",
|
||||||
"tailwind": "4.0.0",
|
"tailwind": "4.0.0",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
@ -31,12 +32,14 @@
|
|||||||
"@angular/cli": "^19.0.5",
|
"@angular/cli": "^19.0.5",
|
||||||
"@angular/compiler-cli": "^19.0.4",
|
"@angular/compiler-cli": "^19.0.4",
|
||||||
"@types/jasmine": "~5.1.5",
|
"@types/jasmine": "~5.1.5",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
"jasmine-core": "~5.2.0",
|
"jasmine-core": "~5.2.0",
|
||||||
"karma": "~6.4.4",
|
"karma": "~6.4.4",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
"karma-coverage": "~2.2.1",
|
"karma-coverage": "~2.2.1",
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
"typescript": "~5.5.4"
|
"typescript": "~5.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,5 @@
|
|||||||
|
<main class="container mx-auto px-4 py-8 max-w-6xl">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 mb-8">{{ title }}</h1>
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
@ -10,5 +10,5 @@ import {RouterOutlet} from '@angular/router';
|
|||||||
styleUrl: './app.component.css'
|
styleUrl: './app.component.css'
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
title = 'lf10StarterNew';
|
title = 'Employee Management System';
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,73 @@
|
|||||||
|
:host ::ng-deep {
|
||||||
|
.mat-mdc-card {
|
||||||
|
--mdc-elevated-card-container-color: transparent;
|
||||||
|
@apply !shadow-none !rounded-xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-button-base {
|
||||||
|
--mat-mdc-button-persistent-ripple-color: currentColor;
|
||||||
|
@apply !rounded-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-progress-spinner {
|
||||||
|
--mdc-circular-progress-active-indicator-color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdc-data-table__header-cell {
|
||||||
|
@apply !text-gray-600 !font-semibold !text-sm !py-4 !px-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-table {
|
||||||
|
@apply !bg-transparent !border-separate !border-spacing-y-2;
|
||||||
|
|
||||||
|
.mat-mdc-row {
|
||||||
|
@apply !bg-white !rounded-xl !shadow-sm !transition-all !duration-200;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply !bg-gray-50 !shadow-md !transform !scale-[1.01];
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-cell {
|
||||||
|
@apply !border-b-0 !py-4 !px-6 first:!rounded-l-xl last:!rounded-r-xl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-header-row {
|
||||||
|
@apply !bg-transparent;
|
||||||
|
|
||||||
|
.mat-mdc-header-cell {
|
||||||
|
@apply !border-b-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-menu-panel {
|
||||||
|
@apply !rounded-xl !shadow-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-menu-item {
|
||||||
|
@apply !rounded-lg !mx-1 !my-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdc-button {
|
||||||
|
@apply !font-medium;
|
||||||
|
|
||||||
|
&.mat-primary {
|
||||||
|
@apply !bg-blue-600 !text-white hover:!bg-blue-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mat-warn {
|
||||||
|
@apply !bg-red-600 !text-white hover:!bg-red-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-snack-bar-container {
|
||||||
|
&.error-snackbar {
|
||||||
|
@apply !rounded-xl;
|
||||||
|
|
||||||
|
.mdc-snackbar__surface {
|
||||||
|
@apply !bg-red-50 !text-red-900 !border !border-red-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,94 @@
|
|||||||
<h1>LF10-Starter</h1>
|
<section class="!space-y-6">
|
||||||
Wenn Sie in der EmployeeListComponent.ts ein gültiges Bearer-Token eintragen, sollten hier die Namen der in der Datenbank gespeicherten Mitarbeiter angezeigt werden!
|
@defer {
|
||||||
<ul>
|
@if (employees$ | async; as employees) {
|
||||||
@for(e of employees$ | async; track e.id) {
|
<div class="!space-y-6">
|
||||||
<li>
|
<div class="!flex !justify-between !items-center">
|
||||||
{{e.lastName }}, {{e.firstName}}
|
<h2 class="!text-2xl !font-semibold !text-gray-900">Employee Directory</h2>
|
||||||
</li>
|
<button mat-flat-button color="primary" class="!bg-blue-600 !text-white">
|
||||||
|
<mat-icon class="!mr-2">add</mat-icon>
|
||||||
|
Add Employee
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (employees) {
|
||||||
|
<div class="!overflow-x-auto !rounded-lg !bg-gray-50 !p-4">
|
||||||
|
<table mat-table [dataSource]="employees" matSort class="!w-full">
|
||||||
|
<ng-container matColumnDef="name">
|
||||||
|
<th mat-header-cell *matHeaderCellDef class="!text-left"> Name </th>
|
||||||
|
<td mat-cell *matCellDef="let employee" class="!py-4">
|
||||||
|
<div class="!flex !items-center">
|
||||||
|
<div class="!h-10 !w-10 !rounded-full !bg-blue-100 !flex !items-center !justify-center !mr-3">
|
||||||
|
<span class="!text-blue-600 !font-medium">
|
||||||
|
{{employee.firstName[0]}}{{employee.lastName[0]}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="!font-medium !text-gray-900">
|
||||||
|
{{employee.lastName}}, {{employee.firstName}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="actions">
|
||||||
|
<th mat-header-cell *matHeaderCellDef class="!text-right"> Actions </th>
|
||||||
|
<td mat-cell *matCellDef="let employee" class="!text-right !py-4">
|
||||||
|
<button mat-icon-button color="primary" [matTooltip]="'Edit employee'" class="!mr-2">
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button color="warn" [matTooltip]="'Delete employee'">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<mat-card class="!text-center !py-8">
|
||||||
|
<mat-card-content>
|
||||||
|
<mat-icon class="!w-12 !h-12 !text-gray-400 !mb-4">people_outline</mat-icon>
|
||||||
|
<p class="!text-gray-600">No employees found</p>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
}
|
}
|
||||||
</ul>
|
</div>
|
||||||
|
}
|
||||||
|
} @placeholder {
|
||||||
|
<div class="!space-y-6">
|
||||||
|
<div class="!animate-pulse">
|
||||||
|
<div class="!flex !justify-between !items-center !mb-8">
|
||||||
|
<div class="!h-8 !bg-gray-200 !rounded !w-1/4"></div>
|
||||||
|
<div class="!h-10 !bg-gray-200 !rounded !w-32"></div>
|
||||||
|
</div>
|
||||||
|
<div class="!bg-gray-50 !p-4 !rounded-lg">
|
||||||
|
<div class="!space-y-4">
|
||||||
|
<div class="!h-10 !bg-gray-200 !rounded"></div>
|
||||||
|
@for(i of [1,2,3]; track i) {
|
||||||
|
<div class="!h-16 !bg-white !rounded-lg !shadow-sm"></div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @error {
|
||||||
|
<mat-card class="!bg-red-50 !border !border-red-100">
|
||||||
|
<mat-card-content class="!p-4">
|
||||||
|
<div class="!flex !items-center !gap-4 !text-red-800">
|
||||||
|
<mat-icon class="!text-red-500">error_outline</mat-icon>
|
||||||
|
<div>
|
||||||
|
<h3 class="!font-medium !mb-1">Error loading employees</h3>
|
||||||
|
<p class="!text-sm !text-red-700">Please try refreshing the page.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
} @loading (minimum 500ms) {
|
||||||
|
<div class="!flex !justify-center !items-center !py-12">
|
||||||
|
<mat-spinner diameter="48" class="!text-blue-600"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
@ -1,25 +1,74 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import {Observable, of} from "rxjs";
|
import { Observable, catchError, of, retry } from 'rxjs';
|
||||||
import {HttpClient, HttpHeaders} from "@angular/common/http";
|
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||||
import {Employee} from "../Employee";
|
import { Employee } from '../Employee';
|
||||||
|
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
|
import { MatDividerModule } from '@angular/material/divider';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-employee-list',
|
selector: 'app-employee-list',
|
||||||
imports: [CommonModule],
|
|
||||||
templateUrl: './employee-list.component.html',
|
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
MatSnackBarModule,
|
||||||
|
MatDividerModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
MatMenuModule,
|
||||||
|
MatTableModule,
|
||||||
|
MatSortModule
|
||||||
|
],
|
||||||
|
templateUrl: './employee-list.component.html',
|
||||||
|
host: {
|
||||||
|
class: 'block w-full p-6'
|
||||||
|
},
|
||||||
styleUrl: './employee-list.component.css'
|
styleUrl: './employee-list.component.css'
|
||||||
})
|
})
|
||||||
export class EmployeeListComponent {
|
export class EmployeeListComponent {
|
||||||
employees$: Observable<Employee[]>;
|
private static readonly EMPLOYEES_ENDPOINT = 'http://localhost:8089/employees';
|
||||||
|
private static readonly MAX_RETRIES = 3;
|
||||||
|
public employees$: Observable<Employee[]>;
|
||||||
|
public readonly displayedColumns: string[] = ['name', 'actions'];
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
constructor(
|
||||||
this.employees$ = of([]);
|
private readonly httpClient: HttpClient,
|
||||||
this.fetchData();
|
private readonly snackBar: MatSnackBar
|
||||||
|
) {
|
||||||
|
this.employees$ = this.fetchEmployees();
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchData() {
|
private fetchEmployees(): Observable<Employee[]> {
|
||||||
this.employees$ = this.http.get<Employee[]>('http://localhost:8089/employees');
|
return this.httpClient.get<Employee[]>(
|
||||||
|
EmployeeListComponent.EMPLOYEES_ENDPOINT,
|
||||||
|
).pipe(
|
||||||
|
retry(EmployeeListComponent.MAX_RETRIES),
|
||||||
|
catchError((error: HttpErrorResponse) => {
|
||||||
|
console.error('Error fetching employees:', error);
|
||||||
|
this.showErrorMessage('Failed to load employees. Please try again.');
|
||||||
|
return of([]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private showErrorMessage(message: string): void {
|
||||||
|
this.snackBar.open(message, 'Close', {
|
||||||
|
duration: 5000,
|
||||||
|
horizontalPosition: 'end',
|
||||||
|
verticalPosition: 'bottom',
|
||||||
|
panelClass: ['!bg-red-50', '!text-red-900', '!border', '!border-red-100']
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ export class AuthGuardService {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
16
src/app/services/auth.service.spec.ts
Normal file
16
src/app/services/auth.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
|
describe('AuthService', () => {
|
||||||
|
let service: AuthService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(AuthService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -1,4 +1,6 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
html, body { height: 100%; }
|
html, body { height: 100%; }
|
||||||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
||||||
|
11
tailwind.config.js
Normal file
11
tailwind.config.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./src/**/*.{html,ts,css,scss,sass,less,styl}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
Reference in New Issue
Block a user