update table styling

This commit is contained in:
Hop In, I Have Puppies AND WiFi 2024-12-18 12:14:51 +01:00
parent f60c55d600
commit 11473505df
Signed by: jleibl
GPG Key ID: E7B6F77BF5EDB6F7
7 changed files with 259 additions and 25 deletions

View File

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

View File

@ -10,5 +10,5 @@ import {RouterOutlet} from '@angular/router';
styleUrl: './app.component.css'
})
export class AppComponent {
title = 'lf10StarterNew';
title = 'Employee Management System';
}

View File

@ -1,9 +1,96 @@
<h1>LF10-Starter</h1>
Wenn Sie in der EmployeeListComponent.ts ein gültiges Bearer-Token eintragen, sollten hier die Namen der in der Datenbank gespeicherten Mitarbeiter angezeigt werden!
<ul>
@for(e of employees$ | async; track e.id) {
<li>
{{e.lastName }}, {{e.firstName}}
</li>
<section class="!space-y-6">
@defer {
@if (employees$ | async; as employees) {
<div class="!space-y-6">
<div class="!flex !justify-between !items-center">
<h2 class="!text-2xl !font-semibold !text-gray-900">Employee Directory</h2>
<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.length) {
<div class="!overflow-x-auto !rounded-lg !bg-gray-50 !p-4">
<table mat-table [dataSource]="employees" matSort class="!w-full">
<!-- Name Column -->
<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>
<!-- Actions Column -->
<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>

View File

@ -1,30 +1,171 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import {Observable, of} from "rxjs";
import {HttpClient, HttpHeaders} from "@angular/common/http";
import {Employee} from "../Employee";
import { Observable, catchError, map, of, retry, switchMap } from 'rxjs';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Employee } from '../Employee';
import { AuthService } from '../services/auth.service';
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, Sort } from '@angular/material/sort';
@Component({
selector: 'app-employee-list',
imports: [CommonModule],
templateUrl: './employee-list.component.html',
standalone: true,
styleUrl: './employee-list.component.css'
})
export class EmployeeListComponent {
bearer = 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIzUFQ0dldiNno5MnlQWk1EWnBqT1U0RjFVN0lwNi1ELUlqQWVGczJPbGU0In0.eyJleHAiOjE3MzM5MTQ5MjgsImlhdCI6MTczMzkxMTMyOCwianRpIjoiMjNhYzMwMmUtYmYxNS00OTRmLWJhYTItNjIzODllYWZkMmZhIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay5zenV0LmRldi9hdXRoL3JlYWxtcy9zenV0IiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjU1NDZjZDIxLTk4NTQtNDMyZi1hNDY3LTRkZTNlZWRmNTg4OSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImVtcGxveWVlLW1hbmFnZW1lbnQtc2VydmljZSIsInNlc3Npb25fc3RhdGUiOiI2ODdiMTEwYS00NTRjLTQwMzgtYjBkMS1kZDAzZGQ1N2JiNjEiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly9sb2NhbGhvc3Q6NDIwMCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsicHJvZHVjdF9vd25lciIsIm9mZmxpbmVfYWNjZXNzIiwiZGVmYXVsdC1yb2xlcy1zenV0IiwidW1hX2F1dGhvcml6YXRpb24iLCJ1c2VyIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByZWZlcnJlZF91c2VybmFtZSI6InVzZXIifQ.E5ir1Z-POpUU_jvTh8CzoMYO74qo_7uQXw7QQBUvXB2_37pT3_tutAq6sM4V5cNBu--fWar5bltlNcOAWd_7Kdb66Qc23i0RR9vPneoSduJAzoD8gtFbx8c7ltNR4pG-c6tdnkGhLLqM621DShaSlH8Shp-Z0-y4Iq3GFdQrAFH1CrRVYlW0qFv1EZsE9BmhW3hJwrR1S2IPiEN6MwhehLflLa_ZgLcF417ocIfK-6gbbRNAwXA-JajFVOZAEVXs-52Ta9Kb_EEQFpRsjXorfflmbizQmgrbhBUB7MTiPYIcRruZSYdfmjcE008PHnut52cTcVYEuOrUCUqY4VmhoQ';
employees$: Observable<Employee[]>;
constructor(private http: HttpClient) {
this.employees$ = of([]);
this.fetchData();
imports: [
CommonModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatDividerModule,
MatTooltipModule,
MatMenuModule,
MatTableModule,
MatSortModule
],
templateUrl: './employee-list.component.html',
host: {
class: 'block w-full p-6'
},
styles: [`
:host ::ng-deep {
.mat-mdc-card {
--mdc-elevated-card-container-color: transparent;
@apply !shadow-none !rounded-xl;
}
fetchData() {
this.employees$ = this.http.get<Employee[]>('/backend/employees', {
headers: new HttpHeaders()
.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;
}
}
}
}
`]
})
export class EmployeeListComponent {
private static readonly EMPLOYEES_ENDPOINT = '/backend/employees';
private static readonly MAX_RETRIES = 3;
public employees$: Observable<Employee[]>;
public readonly displayedColumns: string[] = ['name', 'actions'];
bearer = 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIzUFQ0dldiNno5MnlQWk1EWnBqT1U0RjFVN0lwNi1ELUlqQWVGczJPbGU0In0.eyJleHAiOjE3MzM5MTQ5MjgsImlhdCI6MTczMzkxMTMyOCwianRpIjoiMjNhYzMwMmUtYmYxNS00OTRmLWJhYTItNjIzODllYWZkMmZhIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay5zenV0LmRldi9hdXRoL3JlYWxtcy9zenV0IiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjU1NDZjZDIxLTk4NTQtNDMyZi1hNDY3LTRkZTNlZWRmNTg4OSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImVtcGxveWVlLW1hbmFnZW1lbnQtc2VydmljZSIsInNlc3Npb25fc3RhdGUiOiI2ODdiMTEwYS00NTRjLTQwMzgtYjBkMS1kZDAzZGQ1N2JiNjEiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly9sb2NhbGhvc3Q6NDIwMCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsicHJvZHVjdF9vd25lciIsIm9mZmxpbmVfYWNjZXNzIiwiZGVmYXVsdC1yb2xlcy1zenV0IiwidW1hX2F1dGhvcml6YXRpb24iLCJ1c2VyIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByZWZlcnJlZF91c2VybmFtZSI6InVzZXIifQ.E5ir1Z-POpUU_jvTh8CzoMYO74qo_7uQXw7QQBUvXB2_37pT3_tutAq6sM4V5cNBu--fWar5bltlNcOAWd_7Kdb66Qc23i0RR9vPneoSduJAzoD8gtFbx8c7ltNR4pG-c6tdnkGhLLqM621DShaSlH8Shp-Z0-y4Iq3GFdQrAFH1CrRVYlW0qFv1EZsE9BmhW3hJwrR1S2IPiEN6MwhehLflLa_ZgLcF417ocIfK-6gbbRNAwXA-JajFVOZAEVXs-52Ta9Kb_EEQFpRsjXorfflmbizQmgrbhBUB7MTiPYIcRruZSYdfmjcE008PHnut52cTcVYEuOrUCUqY4VmhoQ';
constructor(
private readonly httpClient: HttpClient,
private readonly authService: AuthService,
private readonly snackBar: MatSnackBar
) {
this.employees$ = this.initializeEmployeesStream();
}
private initializeEmployeesStream(): Observable<Employee[]> {
return of(this.bearer).pipe(
retry(EmployeeListComponent.MAX_RETRIES),
catchError((error: HttpErrorResponse) => {
console.error('Error fetching auth token:', error);
this.showErrorMessage('Failed to authenticate. Please try again.');
return of([]);
}),
switchMap(token => this.fetchEmployees(token as string))
);
}
private fetchEmployees(token: string): Observable<Employee[]> {
const headers = this.createAuthenticatedHeaders(token);
return this.httpClient.get<Employee[]>(
EmployeeListComponent.EMPLOYEES_ENDPOINT,
{ headers }
).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 createAuthenticatedHeaders(token: string): HttpHeaders {
return new HttpHeaders()
.set('Content-Type', 'application/json')
.set('Authorization', `Bearer ${this.bearer}`)
.set('Authorization', `Bearer ${token}`);
}
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']
});
}
}

View File

@ -1,4 +1,7 @@
/* You can add global styles to this file, and also import other style files */
@tailwind base;
@tailwind components;
@tailwind utilities;
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }

View File

@ -1,7 +1,7 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.{html,ts,css,scss,sass,less,styl,.component.html}',
'./src/**/*.{html,ts,css,scss,sass,less,styl}',
],
theme: {
extend: {},