Compare commits

..

5 commits

Author SHA1 Message Date
Jan-Marlon Leibl
6d520d4467 add angular ws stuff idk 2024-12-17 08:53:49 +01:00
Jan-Marlon Leibl
a6829fc3f5 add better validation to forms 2024-12-03 09:23:50 +01:00
Jan-Marlon Leibl
6e5eadd3a2 add better validation to forms 2024-12-03 09:20:59 +01:00
Jan-Marlon Leibl
074da034c5 add tags functionality 2024-11-26 09:51:53 +01:00
Jan-Marlon Leibl
dd4faea712 update styling and do stuff 2024-11-26 09:37:20 +01:00
60 changed files with 2992 additions and 4538 deletions

View file

@ -16,7 +16,9 @@
"outputPath": "dist/hotel-manager",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"assets": [
{
@ -25,7 +27,10 @@
},
"src/assets"
],
"styles": ["src/styles.css"],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.css"
],
"scripts": []
},
"configurations": {
@ -70,7 +75,10 @@
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": ["zone.js", "zone.js/testing"],
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
{
@ -78,7 +86,10 @@
"input": "public"
}
],
"styles": ["src/styles.css"],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.css"
],
"scripts": []
}
}

BIN
bun.lockb

Binary file not shown.

5928
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,23 +10,25 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^19.0.0",
"@angular/common": "^19.0.0",
"@angular/compiler": "^19.0.0",
"@angular/core": "^19.0.0",
"@angular/forms": "^19.0.0",
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
"@angular/animations": "^18.2.3",
"@angular/cdk": "^18.2.3",
"@angular/common": "^18.2.3",
"@angular/compiler": "^18.2.3",
"@angular/core": "^18.2.3",
"@angular/forms": "^18.2.3",
"@angular/material": "^18.2.3",
"@angular/platform-browser": "^18.2.3",
"@angular/platform-browser-dynamic": "^18.2.3",
"@angular/router": "^18.2.3",
"angular-in-memory-web-api": "^0.18.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
"zone.js": "~0.14.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.0.2",
"@angular/cli": "^19.0.2",
"@angular/compiler-cli": "^19.0.0",
"@angular-devkit/build-angular": "^18.2.3",
"@angular/cli": "^18.2.3",
"@angular/compiler-cli": "^18.2.3",
"@types/jasmine": "~5.1.0",
"autoprefixer": "^10.4.20",
"jasmine-core": "~5.1.0",
@ -39,4 +41,4 @@
"tailwindcss": "^3.4.10",
"typescript": "~5.5.2"
}
}
}

View file

@ -1,4 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["local>Renovate/renovate-config"]
"extends": [
"local>Renovate/renovate-config"
]
}

View file

@ -1,6 +0,0 @@
<button
type="button"
class="px-6 py-2 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75"
>
{{ text }}
</button>

View file

@ -1,10 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-button',
standalone: true,
templateUrl: './button.component.html',
})
export class ButtonComponent {
public text = 'Test';
}

View file

@ -1,12 +0,0 @@
<h1 class="text-2xl font-bold mb-4">Child</h1>
<p class="text-lg mb-2">Balance: {{ balance }}</p>
<p [class.hidden]="!isBroke" class="text-red-500 mb-4">
I'm broke. Now I'm about as poor as Jan-Marlon.
</p>
<button
type="button"
(click)="takeMoney()"
class="px-6 py-2 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75"
>
Take Money
</button>

View file

@ -1,25 +0,0 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
templateUrl: './child.component.html',
})
export class ChildComponent {
@Input() public balance: number = 0;
@Output() balanceChange = new EventEmitter<number>();
public isBroke = false;
public takeMoney() {
if (this.balance <= 0) {
this.isBroke = true;
} else {
this.balance -= 50;
if (this.balance == 0) {
this.isBroke = true;
}
this.balanceChange.emit(this.balance);
}
}
}

View file

@ -1,37 +0,0 @@
<p class="text-2xl font-bold text-gray-800 mb-2">Name: {{ hotel.hotelName }}</p>
<p class="text-base text-gray-600 mb-4">Description: {{ hotel.description }}</p>
<p class="text-lg font-medium text-green-600 mb-4">
Price:
{{
hotel.price
| currency
: getCurrencyCode(selectedLanguage)
: "symbol"
: "2.2-2"
: selectedLanguage
}}
</p>
<select
[ngModel]="selectedLanguage"
(ngModelChange)="languageChange($event)"
class="input-field block w-full p-2 border border-gray-300 rounded-md mb-4 focus:border-blue-500 focus:ring focus:ring-blue-200"
>
<option
*ngFor="let lang of langs"
[value]="lang.code"
[selected]="lang.lang == 'de'"
>
{{ lang.lang }}
</option>
</select>
<img
src="{{ hotel.imageUrl }}"
alt="Hotel"
class="w-full rounded-full h-auto animate-shake shadow-lg mb-4"
/>
<a
*ngIf="!isDetail"
routerLink="/hotels/{{ hotel.id }}"
class="submit-button text-blue-600 hover:text-blue-800 hover:underline mb-4 block"
>Details</a
>

View file

@ -1,63 +0,0 @@
import { Component, Injectable, Input } from '@angular/core';
import { ChildComponent } from '../Child/child.component';
import { Hotel } from './hotel';
import { CurrencyPipe, NgFor, NgIf } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { StarRatingComponent } from '../star-rating/star-rating.component';
import { HttpClient } from '@angular/common/http';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-hotel-item',
standalone: true,
templateUrl: './HotelItem.component.html',
imports: [
ChildComponent,
CurrencyPipe,
FormsModule,
StarRatingComponent,
NgIf,
RouterLink,
NgFor,
],
})
export class HotelItem {
@Input() public hotel!: Hotel;
public selectedLanguage?: string;
@Input() public isDetail: boolean = false;
public languageChange(lang: string) {
this.selectedLanguage = lang;
console.log(this.selectedLanguage);
}
public getCurrencyCode(langCode: string | undefined): string {
if (!langCode) return '';
for (let language of this.langs) {
if (language.code === langCode) {
return language.currency;
}
}
return '';
}
public langs = [
{
lang: 'en',
code: 'en-US',
currency: 'USD',
},
{
lang: 'cn',
code: 'cn-CN',
currency: 'CNY',
},
{
lang: 'de',
code: 'de-DE',
currency: 'EUR',
},
];
}

View file

@ -1,8 +0,0 @@
<div class="m-3">
<h1>Parent</h1>
<p>Kontostand: {{ balance }}</p>
<button type="button" class="button" (click)="addFifty()">Add Money</button>
<app-child [(balance)]="balance"></app-child>
<app-child [(balance)]="balance"></app-child>
</div>

View file

@ -1,16 +0,0 @@
import { Component } from '@angular/core';
import { ChildComponent } from '../Child/child.component';
@Component({
selector: 'app-parent',
standalone: true,
templateUrl: './parent.component.html',
imports: [ChildComponent],
})
export class ParentComponent {
public balance = 1000;
public addFifty() {
this.balance += 50;
}
}

View file

@ -1,47 +0,0 @@
import { Injectable } from '@angular/core';
import { Hotel } from '../../HotelItem/hotel';
import { from, Observable } from 'rxjs';
@Injectable()
export class HotelService {
public getHotels(): Observable<Hotel> {
return from([
{
id: 1,
hotelName: 'Buea süßes Leben',
description: 'Schöne Aussicht am Meer',
price: 230.5,
imageUrl: 'assets/img/1.jpg',
rating: 3.5,
tags: ['test'],
},
{
id: 2,
hotelName: 'Marrakesch',
description: 'Genießen Sie den Blick auf die Berge',
price: 145.5,
imageUrl: 'assets/img/2.jpg',
rating: 5,
tags: ['test'],
},
{
id: 3,
hotelName: 'Abuja neuer Palast',
description: 'Kompletter Aufenthalt mit Autoservice',
price: 120.12,
imageUrl: 'assets/img/3.jpg',
rating: 4,
tags: ['test'],
},
{
id: 4,
hotelName: 'Kapstadt Stadt',
description: 'Wunderschönes Ambiente für Ihren Aufenthalt',
price: 135.12,
imageUrl: 'assets/img/4.jpg',
rating: 2.5,
tags: ['test'],
},
]);
}
}

View file

@ -1,6 +1,2 @@
<input
[(ngModel)]="input"
(ngModelChange)="update($event)"
class="border border-gray-300 rounded-md p-2 w-full focus:border-blue-500 focus:ring focus:ring-blue-200"
type="search"
/>
<label for="search">Search</label>
<input [(ngModel)]="input" (ngModelChange)="update($event)" id="search" class="border border-gray-300 rounded-md p-2 w-full focus:border-blue-500 focus:ring focus:ring-blue-200" type="search">

View file

@ -1,8 +1,6 @@
import { CommonModule } from '@angular/common';
import { Component, NgModule } from '@angular/core';
import { FormsModule, NgForm, NgModel } from '@angular/forms';
import { Input, Output } from '@angular/core';
import { EventEmitter } from '@angular/core';
import { Component, Input, Output } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { EventEmitter } from "@angular/core";
@Component({
selector: 'app-search',
@ -11,7 +9,7 @@ import { EventEmitter } from '@angular/core';
imports: [FormsModule],
})
export class SearchComponent {
@Input() public input: string = '';
@Input() public input: string = "";
@Output() inputChange = new EventEmitter<string>();
public update(e: string) {

View file

@ -1,6 +1,6 @@
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { Hotel } from '../HotelItem/hotel';
import { Hotel } from '../hotel-item/hotel';
/**
* Initial data for in memory web api
@ -10,6 +10,7 @@ import { Hotel } from '../HotelItem/hotel';
* @implements {InMemoryDbService}
*/
export class HotelData implements InMemoryDbService {
createDb(): Record<string, Hotel[]> {
const hotels: Hotel[] = [
{
@ -19,43 +20,39 @@ export class HotelData implements InMemoryDbService {
price: 230.5,
imageUrl: 'assets/img/1.jpg',
rating: 3.5,
tags: ['nouveau'],
},
{
tags: ['nouveau']
}, {
id: 2,
hotelName: 'Marakech',
description: 'Profitez de la vue sur les montagnes',
price: 145.5,
imageUrl: 'assets/img/2.jpg',
rating: 5,
tags: ['nouveau'],
},
{
tags: ['nouveau']
}, {
id: 3,
hotelName: 'Abudja new look palace',
description: 'Séjour complet avec service de voitures',
price: 120.12,
imageUrl: 'assets/img/3.jpg',
rating: 4,
tags: ['nouveau'],
},
{
tags: ['nouveau']
}, {
id: 4,
hotelName: 'Cape town city',
description: 'Magnifique cadre pour votre séjour',
price: 135.12,
imageUrl: 'assets/img/4.jpg',
rating: 2.5,
tags: ['nouveau'],
},
tags: ['nouveau']
}
];
return { hotels };
}
genId(hotels: Hotel[]): number {
return hotels.length > 0
? Math.max(...hotels.map((hotel) => hotel.id)) + 1
: 1;
return hotels.length > 0 ? Math.max(...hotels.map(hotel => hotel.id)) + 1 : 1;
}
}

View file

@ -1,5 +1 @@
<router-outlet></router-outlet>
<!-- <p>ID: {{response.id}}</p> -->
<!-- <p>userId: {{response.userId}}</p> -->
<!-- <p>title: {{response.title}}</p> -->
<!-- <p>completed: {{response.completed}}</p> -->
<router-outlet></router-outlet>

View file

@ -24,8 +24,6 @@ describe('AppComponent', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain(
'Hello, hotel-manager',
);
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, hotel-manager');
});
});

View file

@ -1,38 +1,16 @@
import { Component, Injectable } from '@angular/core';
import { HotelItem } from './HotelItem/HotelItem.component';
import { SearchComponent } from './Search/search.component';
import {
AsyncPipe,
NgFor,
NgForOf,
NgIf,
UpperCasePipe,
} from '@angular/common';
import { TextPipe } from '../text.pipe';
import { HotelService } from './Parent/services/hotel.service';
import { inject } from '@angular/core';
import { filter, from, last, map, Observable, scan } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Hotel } from './HotelItem/hotel';
import { HotelService } from './service/hotel.service';
import { RouterOutlet } from '@angular/router';
@Injectable({ providedIn: 'root' })
@Injectable({providedIn: "root"})
@Component({
selector: 'app-root',
standalone: true,
imports: [
RouterOutlet,
NgFor,
NgForOf,
NgIf,
HotelItem,
SearchComponent,
UpperCasePipe,
TextPipe,
AsyncPipe,
],
imports: [RouterOutlet],
templateUrl: './app.component.html',
providers: [HotelService],
styleUrl: './app.component.css',
styleUrl: './app.component.css'
})
export class AppComponent {}
export class AppComponent {
}

View file

@ -1,8 +1,4 @@
import {
ApplicationConfig,
importProvidersFrom,
provideZoneChangeDetection,
} from '@angular/core';
import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import localeDe from '@angular/common/locales/de';
import localeCn from '@angular/common/locales/zh-Hans';
@ -12,6 +8,7 @@ import { registerLocaleData } from '@angular/common';
import { provideHttpClient } from '@angular/common/http';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { HotelData } from './api/api';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
registerLocaleData(localeDe, 'de-DE');
registerLocaleData(localeCn, 'cn-CN');
@ -20,6 +17,5 @@ export const appConfig: ApplicationConfig = {
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(),
importProvidersFrom(InMemoryWebApiModule.forRoot(HotelData)),
],
importProvidersFrom(InMemoryWebApiModule.forRoot(HotelData)), provideAnimationsAsync()]
};

View file

@ -1,24 +1,27 @@
import { Routes } from '@angular/router';
import { HotelDetailsComponent } from './hotel-details/hotel-details.component';
import { HotelListComponent } from './hotel-list/hotel-list.component';
import { TestComponent } from './test/test.component';
import { NewHotelComponent } from './new-hotel/new-hotel.component';
import { HotelFormComponent } from './hotel-form/hotel-form.component';
import { formGuard } from './form.guard';
import { NotFoundComponent } from './not-found/not-found.component';
export const routes: Routes = [
{
path: '',
path: "",
component: HotelListComponent,
},
{
path: 'hotels/:id',
path: "hotels/:id",
component: HotelDetailsComponent,
canDeactivate: [formGuard],
},
{
path: 'testing',
component: TestComponent,
path: "create-hotel",
component: HotelFormComponent,
canDeactivate: [formGuard],
},
{
path: 'new',
component: NewHotelComponent,
path: "**",
component: NotFoundComponent,
},
];

View file

@ -1,36 +0,0 @@
import {
AbstractControl,
FormGroup,
ValidationErrors,
ValidatorFn,
} from '@angular/forms';
export class CrossValidator {
public crossValidate() {
return (control: AbstractControl): ValidationErrors | null => {
if (!(control instanceof FormGroup)) {
return null;
}
const contactType = control.get('contactType')?.value;
if (contactType == ('None' || null)) return null;
let error = null;
if (contactType == 'Email') {
error =
control.get('email')?.value != control.get('emailConfirmation')?.value
? { mismatchEmail: true }
: null;
} else {
error =
control.get('phone')?.value != control.get('phoneConfirmation')?.value
? { mismatchPhone: true }
: null;
}
console.log('Error: ', error);
return error;
};
}
}

View file

@ -0,0 +1,17 @@
import { TestBed } from '@angular/core/testing';
import { CanDeactivateFn } from '@angular/router';
import { formGuard } from './form.guard';
describe('formGuard', () => {
const executeGuard: CanDeactivateFn = (...guardParameters) =>
TestBed.runInInjectionContext(() => formGuard(...guardParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeGuard).toBeTruthy();
});
});

15
src/app/form.guard.ts Normal file
View file

@ -0,0 +1,15 @@
import { CanDeactivateFn } from '@angular/router';
import { HotelFormComponent } from './hotel-form/hotel-form.component';
import { HotelDetailsComponent } from './hotel-details/hotel-details.component';
export const formGuard: CanDeactivateFn<HotelFormComponent | HotelDetailsComponent> = (component: HotelFormComponent | HotelDetailsComponent, currentRoute, currentState, nextState) => {
const hasUnsavedChanges =
(component instanceof HotelFormComponent && component.form?.dirty) ||
(component instanceof HotelDetailsComponent && component.hotelForm?.form?.dirty);
if (hasUnsavedChanges) {
return confirm('You have unsaved changes. Do you really want to leave?');
}
return true;
};

View file

@ -1,5 +1,5 @@
@if (hotel) {
<app-hotel-item [hotel]="hotel" [isDetail]="true"></app-hotel-item>
<app-hotel-form [hotel]="hotel"></app-hotel-form>
<button class="delete-button" (click)="delete()">😠 DELETE</button>
}
<div class="container p-4 mx-auto max-w-4xl">
<div *ngIf="hotel">
<app-hotel-form [hotel]="hotel" #hotelForm></app-hotel-form>
</div>
</div>

View file

@ -8,8 +8,9 @@ describe('HotelDetailsComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HotelDetailsComponent],
}).compileComponents();
imports: [HotelDetailsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HotelDetailsComponent);
component = fixture.componentInstance;

View file

@ -1,61 +1,39 @@
import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Hotel } from '../HotelItem/hotel';
import { CurrencyPipe, NgFor, NgForOf, NgIf } from '@angular/common';
import { StarRatingComponent } from '../star-rating/star-rating.component';
import { HotelItem } from '../HotelItem/HotelItem.component';
import { Hotel } from '../hotel-item/hotel';
import { NgIf } from '@angular/common';
import { catchError, EMPTY } from 'rxjs';
import { HotelFormComponent } from '../hotel-form/hotel-form.component';
import { HotelFormComponent } from "../hotel-form/hotel-form.component";
@Component({
selector: 'app-hotel-details',
standalone: true,
imports: [
CurrencyPipe,
StarRatingComponent,
HotelItem,
HotelFormComponent,
NgIf,
NgForOf,
],
imports: [HotelFormComponent, NgIf],
templateUrl: './hotel-details.component.html',
styleUrl: './hotel-details.component.css',
styleUrl: './hotel-details.component.css'
})
export class HotelDetailsComponent implements OnInit {
public hotel: any;
constructor(
private route: ActivatedRoute,
private http: HttpClient,
private router: Router,
) {}
@ViewChild('hotelForm', { static: false })
public hotelForm!: HotelFormComponent;
delete(): void {
if (confirm('Are u sure u want to delete this hotel')) {
this.http
.delete<Hotel>('/api/hotels/' + this.route.snapshot.paramMap.get('id'))
.subscribe();
this.router.navigate(['/']);
}
}
constructor(private route: ActivatedRoute, private http: HttpClient, private router: Router) { }
ngOnInit(): void {
const routeParams = this.route.snapshot.paramMap;
const hotelId = routeParams.get('id');
const hotelId = routeParams.get("id");
this.http
.get<Hotel>('api/hotels/' + hotelId)
.pipe(
catchError(() => {
alert('Not Found');
this.router.navigate(['/']);
this.http.get<Hotel>("api/hotels/" + hotelId).pipe(
catchError(() => {
alert("Not Found");
this.router.navigate(["/"]);
return EMPTY;
}),
)
.subscribe((res) => {
this.hotel = res;
});
return EMPTY;
})
).subscribe(res => {
this.hotel = res;
})
}
}

View file

@ -1,47 +1,70 @@
<p>hotel-form works!</p>
<h1>{{ errorMsg }}</h1>
<form [formGroup]="hotelForm">
<label for="name">Name</label>
<div class="text-red-500 text-5xl font-bold" *ngIf="errorMessages['name']">
{{ errorMessages["name"] }}
<form class="space-y-4 max-w-md mx-auto mt-4" [formGroup]="form">
<ng-container *ngFor="let field of ['name', 'description', 'price', 'rating']">
<div class="form-group">
<label [for]="'hotel' + field" class="block text-sm font-medium text-gray-700">
Hotel {{field.charAt(0).toUpperCase() + field.slice(1)}}
</label>
<input [type]="field === 'price' || field === 'rating' ? 'number' : 'text'" [formControlName]="field"
class="form-control mt-1 block w-full p-2 border rounded-md shadow-sm"
[ngClass]="{
'border-red-500': errorMessage.includes(field),
'border-gray-300': !errorMessage.includes(field),
'focus:border-red-500': errorMessage.includes(field),
'focus:border-blue-500': !errorMessage.includes(field),
'focus:ring-red-200': errorMessage.includes(field),
'focus:ring-blue-200': !errorMessage.includes(field)
}"
[id]="'hotel' + field" [placeholder]="'Enter hotel ' + field">
<div *ngIf="errorMessage?.includes(field)" class="text-red-500 text-sm mt-1">
{{errorMessage}}
</div>
</div>
</ng-container>
<div class="form-group">
<label for="hotelTags" class="block text-sm font-medium text-gray-700">Hotel Tags</label>
<div formArrayName="tags">
<div *ngFor="let tag of tags.controls; let i=index" class="flex items-center space-x-2">
<input type="text" [formControlName]="i"
class="form-control mt-1 w-full p-2 border rounded-md shadow-sm"
[ngClass]="{
'border-red-500': errorMessage.includes('tags'),
'border-gray-300': !errorMessage.includes('tags'),
'focus:border-red-500': errorMessage.includes('tags'),
'focus:border-blue-500': !errorMessage.includes('tags'),
'focus:ring-red-200': errorMessage.includes('tags'),
'focus:ring-blue-200': !errorMessage.includes('tags')
}"
[id]="'hotelTag' + i" placeholder="Enter hotel tag">
<button mat-icon-button (click)="tags.removeAt(i)" class="mt-1">
<mat-icon class="text-red-500">delete</mat-icon>
</button>
</div>
<button type="button" mat-button (click)="addTag()" class="mt-2">Add Tag</button>
</div>
<div *ngIf="errorMessage.includes('tags')" class="text-red-500 text-sm mt-1">
{{errorMessage}}
</div>
</div>
<input
type="text"
class="border-red-500 text-3xl border-3 rounded-full bg-gray-500 font-bold p-3 m-3 border-8"
id="name"
formControlName="name"
/>
<label for="description">Description</label>
<div class="text-red" *ngIf="errorMessages['description']">
{{ errorMessages["description"] }}
<div class="flex justify-between">
<div class="flex space-x-1">
<button type="submit" mat-flat-button (click)="submit()">Submit</button>
<button type="button" mat-stroked-button routerLink="/">Cancel</button>
</div>
<button mat-stroked-button color="warn" aria-label="Delete hotel" *ngIf="hotel" (click)="showDeleteConfirmation = true">
Remove
</button>
<div *ngIf="showDeleteConfirmation" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white p-6 rounded-lg shadow-lg max-w-sm w-full">
<h2 class="text-xl font-bold mb-4">Confirm Deletion</h2>
<p class="mb-6">Are you sure you want to delete this hotel?</p>
<div class="flex justify-end space-x-2">
<button mat-stroked-button (click)="showDeleteConfirmation = false">Cancel</button>
<button mat-flat-button color="warn" (click)="delete()">Delete</button>
</div>
</div>
</div>
</div>
<input
type="text"
class="input-field"
[class.border-8]="hotelForm.get('description')?.invalid"
id="description"
formControlName="description"
/>
<label for="price">Price</label>
<div class="text-red" *ngIf="errorMessages['price']">
{{ errorMessages["price"] }}
</div>
<input
type="number"
class="input-field"
[class.border-8]="
hotelForm.get('price')?.invalid && hotelForm.get('price')?.touched
"
id="price"
formControlName="price"
/>
<button
class="submit-button"
(click)="submit()"
[disabled]="!hotelForm.valid"
type="submit"
>
😃 Submit
</button>
<a class="back-button" routerLink="/">😭 Cancel</a>
</form>
</form>

View file

@ -8,8 +8,9 @@ describe('HotelFormComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HotelFormComponent],
}).compileComponents();
imports: [HotelFormComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HotelFormComponent);
component = fixture.componentInstance;

View file

@ -1,90 +1,150 @@
import { Component, Input } from '@angular/core';
import { Hotel } from '../HotelItem/hotel';
import {
AbstractControl,
FormControl,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { NgFor, NgIf } from '@angular/common';
import { Component, inject, Input } from '@angular/core';
import { AbstractControl, FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
import { Hotel } from "../hotel-item/hotel";
import { HttpClient } from '@angular/common/http';
import { Router, RouterLink } from '@angular/router';
import { catchError, debounceTime } from "rxjs";
import { Router, RouterLink } from "@angular/router";
import { NgClass, NgFor, NgIf } from "@angular/common";
import { MatButtonModule } from "@angular/material/button";
import { MatIconModule } from "@angular/material/icon";
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-hotel-form',
standalone: true,
imports: [ReactiveFormsModule, NgIf, NgFor, RouterLink],
imports: [
ReactiveFormsModule,
RouterLink,
NgIf,
MatButtonModule,
MatIconModule,
NgFor,
FormsModule,
NgClass
],
templateUrl: './hotel-form.component.html',
styleUrl: './hotel-form.component.css',
styleUrl: './hotel-form.component.css'
})
export class HotelFormComponent {
@Input() public hotel!: Hotel;
public hotelForm!: FormGroup;
public errorMsg: string = 'testing';
errorMessages: Record<string, string> = {};
public form!: FormGroup;
constructor(
public http: HttpClient,
public router: Router,
) {}
@Input()
public hotel!: Hotel;
private validationErrorMessages: Record<string, string> = {
required: 'This field is required',
minlength: 'The value is too short',
maxlength: 'The value is too long',
pattern: 'The value format is incorrect',
};
public router: Router = inject(Router);
setMessage(value: AbstractControl): void {
if ((value.touched || value.dirty) && value.errors) {
this.errorMsg = Object.keys(value.errors)
.map((key) => {
console.log(this.validationErrorMessages[key]);
return this.validationErrorMessages[key] || `Unknown error: ${key}`;
})
.join(' ');
public showDeleteConfirmation = false;
constructor(private http: HttpClient) { }
public errorMessage: string = '';
ngOnInit() {
this.form = new FormGroup({
name: new FormControl(this.hotel?.hotelName || '', [Validators.minLength(3), Validators.required]),
description: new FormControl(this.hotel?.description || '', [Validators.minLength(3), Validators.required]),
price: new FormControl(this.hotel?.price || '', [Validators.min(0), Validators.required]),
rating: new FormControl(this.hotel?.rating || '', [Validators.min(0), Validators.max(5), Validators.required]),
tags: new FormArray(this.hotel?.tags?.map(tag => new FormControl(tag)) || [])
});
Object.keys(this.form.controls).forEach(key => {
const control = this.form.get(key);
control?.valueChanges.pipe(
debounceTime(1000)
).subscribe(() => {
this.setError(control as AbstractControl, key);
});
});
}
public delete() {
this.http.delete(`api/hotels/${this.hotel?.id}`).pipe(
catchError(err => {
console.error('Error deleting hotel', err);
return err;
})
).subscribe(() => {
console.log('Hotel deleted');
});
this.router.navigate(['/']);
}
get tags() {
return this.form.get('tags') as FormArray;
}
public addTag() {
this.tags.push(new FormControl(''));
}
public submit() {
if (this.form.valid && !this.errorMessage) {
const hotelData: Hotel = {
id: this.hotel?.id || 0,
hotelName: this.form.value.name,
description: this.form.value.description,
price: this.form.value.price,
rating: this.form.value.rating,
imageUrl: this.hotel?.imageUrl || 'https://placehold.co/2000x1050/EEE/31343C',
tags: this.form.value.tags || []
};
if (this.hotel) {
this.http.put(`api/hotels/${this.hotel.id}`, hotelData).pipe(
catchError(err => {
console.error('Error updating hotel', err);
return err;
})
).subscribe(() => {
console.log('Hotel updated');
});
}
else {
this.http.post('api/hotels', hotelData).pipe(
catchError(err => {
console.error('Error creating hotel', err);
return err;
})
).subscribe(() => {
console.log('Hotel created');
});
}
this.form.reset();
this.router.navigate(['/']);
}
}
updateErrorMessages(): void {
this.errorMessages = {};
private validationErrors: Record<string, string> = {
'required': 'This field is required',
'minlength': 'This field must be at least 3 characters long',
'min': 'This field must be at least 0',
'max': 'This field must be at most 5',
'name.required': 'Hotel name is required',
'name.minlength': 'Hotel name must be at least 3 characters long',
'description.required': 'Hotel description is required',
'description.minlength': 'Hotel description must be at least 3 characters long',
'price.required': 'Hotel price is required',
'price.min': 'Hotel price cannot be negative',
'rating.required': 'Hotel rating is required',
'rating.min': 'Hotel rating must be at least 0',
'rating.max': 'Hotel rating cannot be more than 5',
'tags.required': 'At least one tag is required',
};
Object.keys(this.hotelForm.controls).forEach((field) => {
const control = this.hotelForm.get(field);
if (control && control.errors) {
this.errorMessages[field] = Object.keys(control.errors)
.map(
(errorKey) =>
this.validationErrorMessages[errorKey] ||
`Unknown error: ${errorKey}`,
)
.join(' ');
}
});
}
ngOnInit(): void {
this.hotelForm = new FormGroup({
name: new FormControl(this.hotel.hotelName, Validators.required),
description: new FormControl(this.hotel.description, Validators.required),
price: new FormControl(this.hotel.price, Validators.required),
});
// this.hotelForm.valueChanges.subscribe(data => this.setMessage(this.hotelForm.get("name")!));
// this.hotelForm.valueChanges.subscribe(data => this.setMessage(this.hotelForm.get("description")!));
// this.hotelForm.valueChanges.subscribe(data => this.setMessage(this.hotelForm.get("price")!));
//
this.hotelForm.valueChanges.subscribe(() => this.updateErrorMessages());
}
submit(): void {
this.hotel.hotelName = this.hotelForm.get('name')?.value;
this.hotel.description = this.hotelForm.get('description')?.value;
this.hotel.price = this.hotelForm.get('price')?.value;
console.log(this.hotelForm.value);
this.http.put('/api/hotels/' + this.hotel.id, this.hotel).subscribe();
this.router.navigate(['/']);
private setError(control: AbstractControl, controlName: string) {
if ((control.touched || control.dirty) && control.errors) {
const errors = Object.keys(control.errors).map(key => {
const errorKey = `${controlName}.${key}`;
return this.validationErrors[errorKey] || this.validationErrors[key];
});
this.errorMessage = errors.join(' ');
} else {
this.errorMessage = '';
}
}
}

View file

@ -0,0 +1,8 @@
<p class="text-2xl font-bold text-gray-800 mb-2">Name: {{hotel.hotelName}}</p>
<p class="text-base text-gray-600 mb-4">Description: {{hotel.description}}</p>
<p class="text-lg font-medium text-green-600 mb-4">Price: {{hotel.price | currency : getCurrencyCode(selectedLanguage) : "symbol" : "2.2-2" : selectedLanguage}}</p>
<select [ngModel]="selectedLanguage" (ngModelChange)="languageChange($event)" class="block w-full p-2 border border-gray-300 rounded-md mb-4 focus:border-blue-500 focus:ring focus:ring-blue-200">
<option *ngFor="let lang of langs" [value]="lang.code" [selected]="lang.lang == 'de'">{{lang.lang}}</option>
</select>
<img src="{{hotel.imageUrl}}" alt="Hotel" class="w-full h-auto rounded-lg shadow-lg mb-4">
<a *ngIf="!isDetail" routerLink="/hotels/{{hotel.id}}" class="text-blue-600 hover:text-blue-800 hover:underline mb-4 block">Details</a>

View file

@ -0,0 +1,53 @@
import { Component, Input } from "@angular/core";
import { Hotel } from "./hotel";
import {CurrencyPipe, NgForOf, NgIf} from "@angular/common";
import { FormsModule } from "@angular/forms";
import { RouterLink } from "@angular/router";
@Component({
selector: 'app-hotel-item',
standalone: true,
templateUrl: './HotelItem.component.html',
imports: [CurrencyPipe, FormsModule, NgIf, RouterLink, NgForOf],
})
export class HotelItem {
@Input() public hotel!: Hotel;
public selectedLanguage?: string;
@Input() public isDetail: boolean = false;
public languageChange(lang: string) {
this.selectedLanguage = lang;
console.log(this.selectedLanguage);
}
public getCurrencyCode(langCode: string | undefined): string {
if (!langCode) return '';
for (let language of this.langs) {
if (language.code === langCode) {
return language.currency;
}
}
return '';
}
public langs = [
{
"lang": "en",
"code": "en-US",
"currency": "USD"
},
{
"lang": "cn",
"code": "cn-CN",
"currency": "CNY"
},
{
"lang": "de",
"code": "de-DE",
"currency": "EUR"
}
];
}

View file

@ -6,6 +6,4 @@ export interface Hotel {
imageUrl: string;
rating: number;
tags: Array<string>;
email?: string;
phone?: string;
}

View file

@ -1,13 +1,28 @@
<div class="container p-4 mx-auto max-w-4xl">
<h1>{{ "hello" | uppercase | text }}</h1>
<a class="submit-button" routerLink="/new">CREATE NEW HOTEL</a>
<app-search [(input)]="search"></app-search>
@if (hotels[0].hotelName) {
<div *ngFor="let hotel of hotels">
<app-hotel-item
*ngIf="hotel.hotelName.toLowerCase().includes(search.toLowerCase())"
[hotel]="hotel"
></app-hotel-item>
<div class="fixed top-0 left-0 w-full bg-white/80 backdrop-blur-xl z-50">
<div class="justify-center max-w-4xl container p-4 mx-auto">
<h1 class="">{{'hello' | uppercase | text}}</h1>
<app-search [(input)]="search"></app-search>
<button routerLink="/create-hotel" mat-flat-button
class="btn btn-primary bg-blue-500 text-white font-semibold py-2 px-4 rounded-md shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75 my-4">Create
New Hotel</button>
</div>
}
</div>
<div *ngIf="hotels[0].hotelName" class="mt-64">
<div *ngFor="let hotel of hotels">
<app-hotel-item *ngIf="hotel.hotelName.toLowerCase().includes(search.toLowerCase())" [hotel]="hotel"></app-hotel-item>
</div>
</div>
<div *ngIf="!hotels[0].hotelName" class="mt-64">
<div class="animate-pulse space-y-4">
<div class="h-8 bg-gray-200 rounded w-52"></div>
<div class="space-y-3">
<div class="h-4 bg-gray-200 rounded w-3/4"></div>
<div class="h-4 bg-gray-200 rounded w-11/12"></div>
<div class="h-4 bg-gray-200 rounded w-2/3"></div>
</div>
<div class="h-[300px] bg-gray-200 rounded w-full"></div>
</div>
</div>
</div>

View file

@ -8,8 +8,9 @@ describe('HotelListComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HotelListComponent],
}).compileComponents();
imports: [HotelListComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HotelListComponent);
component = fixture.componentInstance;

View file

@ -1,83 +1,33 @@
import { CommonModule, NgFor, NgIf, UpperCasePipe } from '@angular/common';
import { NgFor, NgIf, UpperCasePipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import { TextPipe } from '../../text.pipe';
import { SearchComponent } from '../Search/search.component';
import { HotelService } from '../Parent/services/hotel.service';
import { Hotel } from '../HotelItem/hotel';
import { SearchComponent } from '../search/search.component';
import { HotelService } from '../service/hotel.service';
import { Hotel } from '../hotel-item/hotel';
import { HttpClient } from '@angular/common/http';
import { filter, from, last, map, Observable, scan } from 'rxjs';
import { HotelItem } from '../HotelItem/HotelItem.component';
import { HotelItem } from '../hotel-item/HotelItem.component';
import { RouterLink } from '@angular/router';
interface User {
name: string;
age: number;
}
import { MatButtonModule } from '@angular/material/button';
@Component({
selector: 'app-hotel-list',
standalone: true,
imports: [
UpperCasePipe,
TextPipe,
SearchComponent,
HotelItem,
NgFor,
NgIf,
RouterLink,
],
imports: [UpperCasePipe, TextPipe, SearchComponent, HotelItem, NgFor, NgIf, RouterLink, MatButtonModule],
templateUrl: './hotel-list.component.html',
styleUrl: './hotel-list.component.css',
styleUrl: './hotel-list.component.css'
})
export class HotelListComponent {
public search: string = '';
public search: string = "";
public hotelService: HotelService = inject(HotelService);
public response: any = null;
public hotels: Array<Hotel> = [{} as Hotel];
constructor(private http: HttpClient) {}
constructor (private http: HttpClient) {
}
ngOnInit() {
this.http.get<Array<Hotel>>('api/hotels').subscribe((res) => {
this.http.get<Array<Hotel>>("api/hotels").subscribe(res => {
this.hotels = res;
});
const users = [
{ name: 'Max', age: 21 },
{ name: 'Peter', age: 31 },
{ name: 'Hans', age: 13 },
{ name: 'Klaus', age: 51 },
{ name: 'Dieter', age: 1 },
{ name: 'Jan-Marlon', age: 3 },
];
const stream: Observable<User> = from(users);
stream
.pipe(
filter((user) => user.age > 18),
scan((acc, user) => acc + user.age, 0),
map((ageSum, index) => ageSum / (index + 1)),
last(),
)
.subscribe(console.log);
// const stream: Observable<number | string> = from([5, 1, 2, 12, 5, 14, 17, 5, "testing"]);
// stream.pipe(
// filter((value) => typeof value === "number"),
// tap((value) => console.log("Zahl:" + value)),
// filter((value: number) => value % 2 === 0),
// tap((value) => console.log("Gerade Zahl: " + value)),
// toArray(),
//).subscribe(console.log);
}
public test() {
console.log(this.search);
}
// public foundHotels = this.hotels.pipe(
// filter((hotel) => hotel.hotelName.includes(this.search)),
// );
}

View file

@ -1,120 +0,0 @@
<p>Create Hotel</p>
<form [formGroup]="hotelForm">
<div class="text-red-500 bg-white m-3" *ngIf="errorMessages['form']">
{{ errorMessages["form"] }}
</div>
<label for="name">Name</label>
<div class="text-red-500" *ngIf="errorMessages['name']">
{{ errorMessages["name"] }}
</div>
<input
type="text"
class="border-red-500"
[class.border-8]="
hotelForm.get('name')?.invalid && hotelForm.get('name')?.touched
"
id="name"
formControlName="name"
/>
<label for="description">Description</label>
<div class="text-red-500" *ngIf="errorMessages['description']">
{{ errorMessages["description"] }}
</div>
<input
type="text"
class="border-red-500"
[class.border-8]="
hotelForm.get('description')?.invalid &&
hotelForm.get('description')?.touched
"
id="description"
formControlName="description"
/>
<label for="imageUrl">Image</label>
<div class="text-red-500" *ngIf="errorMessages['imageUrl']">
{{ errorMessages["imageUrl"] }}
</div>
<input
type="url"
class="border-red-500"
[class.border-8]="
hotelForm.get('imageUel')?.invalid && hotelForm.get('imageUrl')?.touched
"
id="imageUrl"
formControlName="imageUrl"
/>
<label for="price">Price</label>
<div class="text-red-500" *ngIf="errorMessages['price']">
{{ errorMessages["price"] }}
</div>
<input
type="number"
class="border-red-500"
[class.border-8]="hotelForm.get('price')?.invalid"
id="price"
formControlName="price"
/>
<label for="rating">Rating</label>
<div class="text-red-500" *ngIf="errorMessages['rating']">
{{ errorMessages["rating"] }}
</div>
<input
type="rating"
class="border-red-500"
[class.border-8]="hotelForm.get('rating')?.invalid"
id="rating"
formControlName="rating"
/>
<button class="submit-button" (click)="addTag()">Add Tag</button>
@for (tag of getTags().controls; track tag) {
<input
type="tag"
class="border-red-500"
[class.border-8]="hotelForm.get('tag')?.invalid"
id="tag"
formControlName="tag"
/>
<button (click)="deleteTag(tag)" class="delete-button">delete</button>
}
<input type="radio" value="None" formControlName="contactType" />None
<input type="radio" value="Email" formControlName="contactType" />Email
<input type="radio" value="SMS" formControlName="contactType" />SMS
@if (hotelForm.get("contactType")?.value == "Email") {
<div class="mt-3">
<label for="email">Email</label>
<input type="email" formControlName="email" id="email" />
<label for="emailConfirmation">Email Confirmation</label>
<input
type="email"
formControlName="emailConfirmation"
id="emailConfirmation"
/>
</div>
}
@if (hotelForm.get("contactType")?.value == "SMS") {
<div class="mt-3">
<label for="phone">Phone Number</label>
<input type="tel" formControlName="phone" id="phone" />
<label for="phoneConfirmation">Phone Number Confirmation</label>
<input
type="tel"
formControlName="phoneConfirmation"
id="phoneConfirmation"
/>
</div>
}
<button
class="submit-button"
(click)="submit()"
[disabled]="!hotelForm.valid"
type="submit"
>
Submit
</button>
<a routerLink="/" class="delete-button">Cancel</a>
</form>

View file

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

View file

@ -1,139 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';
import {
AbstractControl,
FormArray,
FormControl,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { Hotel } from '../HotelItem/hotel';
import { NgFor, NgIf } from '@angular/common';
import { CrossValidator } from '../crossValidator.service';
@Component({
selector: 'app-new-hotel',
standalone: true,
imports: [ReactiveFormsModule, NgIf, NgFor, RouterLink],
templateUrl: './new-hotel.component.html',
styleUrl: './new-hotel.component.css',
})
export class NewHotelComponent {
public hotelForm!: FormGroup;
public hotel!: Hotel;
errorMessages: Record<string, string> = {};
private validationErrorMessages: Record<string, string> = {
required: 'This field is required',
minlength: 'The value is too short',
maxlength: 'The value is too long',
pattern: 'The value format is incorrect',
};
private fullFormValidationErrorMessages: Record<string, string> = {
mismatch: 'The name and description have to be the same',
mismatchEmail: 'The Emails must be the same.',
mismatchPhone: 'The Phone numbers must be the same.',
};
updateErrorMessages(): void {
this.errorMessages = {};
Object.keys(this.hotelForm.controls).forEach((field) => {
const control = this.hotelForm.get(field);
if (control && control.errors && control.touched) {
this.errorMessages[field] = Object.keys(control.errors)
.map(
(errorKey) =>
this.validationErrorMessages[errorKey] ||
`Unknown error: ${errorKey}`,
)
.join(' ');
}
});
if (this.hotelForm.errors) {
Object.keys(this.hotelForm.errors).forEach((errorKey) => {
const errorMessage =
this.fullFormValidationErrorMessages[errorKey] ||
`Unknown error: ${errorKey}`;
this.errorMessages['form'] = errorMessage;
});
}
console.log(this.errorMessages);
}
ngOnInit(): void {
const tags = [];
for (const tag of this.hotel?.tags ?? []) {
tags.push(new FormControl(tag, [Validators.required]));
}
this.hotelForm = new FormGroup(
{
name: new FormControl('', Validators.required),
description: new FormControl('', Validators.required),
price: new FormControl(0, Validators.required),
imageUrl: new FormControl('', Validators.required),
rating: new FormControl(0, Validators.required),
contactType: new FormControl('None'),
email: new FormControl(''),
emailConfirmation: new FormControl(''),
phone: new FormControl(''),
phoneConfirmation: new FormControl(''),
tags: new FormArray(tags),
},
{ validators: new CrossValidator().crossValidate() },
);
this.hotelForm.valueChanges.subscribe(() => this.updateErrorMessages());
this.hotelForm.valueChanges.subscribe(() => this.formUpdated());
}
formUpdated() {
console.log(this.hotelForm.get('contactType')?.value);
}
getTags() {
return this.hotelForm.controls['tags'] as FormArray;
}
deleteTag(tagElement: AbstractControl) {
this.getTags().removeAt(this.getTags().controls.indexOf(tagElement));
}
addTag() {
this.getTags().push(new FormControl('', [Validators.required]));
}
constructor(
public http: HttpClient,
public router: Router,
) {}
submit(): void {
console.log(this.hotelForm.value);
const hotel: Hotel = {
id: 0,
hotelName: this.hotelForm.get('name')?.value,
description: this.hotelForm.get('description')?.value,
price: this.hotelForm.get('price')?.value,
imageUrl: this.hotelForm.get('imageUrl')?.value,
rating: this.hotelForm.get('rating')?.value,
tags: this.hotelForm.get('tags')?.value,
};
if (this.hotelForm.get('contactType')?.value == 'Email') {
hotel.email = this.hotelForm.get('email')?.value;
} else {
hotel.phone = this.hotelForm.get('phone')?.value;
}
this.http.post('/api/hotels/', hotel).subscribe();
this.router.navigate(['/']);
}
}

View file

@ -0,0 +1,7 @@
<div class="min-h-screen flex items-center justify-center bg-gray-100">
<div class="text-center">
<h1 class="text-9xl font-bold text-gray-800">404</h1>
<p class="text-2xl font-medium text-gray-600 mb-8">Page Not Found</p>
<a type="submit" mat-flat-button routerLink="/">Return Home</a>
</div>
</div>

View file

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

View file

@ -0,0 +1,17 @@
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-not-found',
standalone: true,
imports: [
MatButtonModule,
RouterLink
],
templateUrl: './not-found.component.html',
styleUrl: './not-found.component.css'
})
export class NotFoundComponent {
}

View file

@ -0,0 +1,51 @@
import { Injectable } from "@angular/core";
import { Hotel } from "../hotel-item/hotel";
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Observable, throwError, catchError } from "rxjs";
@Injectable({
providedIn: 'root'
})
export class HotelService {
private apiUrl = 'api/hotels';
constructor(private http: HttpClient) {}
private handleError(error: HttpErrorResponse) {
let errorMessage = 'An error occurred';
if (error.error instanceof ErrorEvent) {
// Client-side error
errorMessage = `Error: ${error.error.message}`;
} else {
// Server-side error
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
}
console.error(errorMessage);
return throwError(() => errorMessage);
}
public getHotels(): Observable<Hotel[]> {
return this.http.get<Hotel[]>(this.apiUrl)
.pipe(catchError(this.handleError));
}
public getHotel(id: number): Observable<Hotel> {
return this.http.get<Hotel>(`${this.apiUrl}/${id}`)
.pipe(catchError(this.handleError));
}
public createHotel(hotel: Hotel): Observable<Hotel> {
return this.http.post<Hotel>(this.apiUrl, hotel)
.pipe(catchError(this.handleError));
}
public updateHotel(hotel: Hotel): Observable<Hotel> {
return this.http.put<Hotel>(`${this.apiUrl}/${hotel.id}`, hotel)
.pipe(catchError(this.handleError));
}
public deleteHotel(id: number): Observable<{}> {
return this.http.delete(`${this.apiUrl}/${id}`)
.pipe(catchError(this.handleError));
}
}

View file

@ -8,8 +8,9 @@ describe('StarRatingComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [StarRatingComponent],
}).compileComponents();
imports: [StarRatingComponent]
})
.compileComponents();
fixture = TestBed.createComponent(StarRatingComponent);
component = fixture.componentInstance;

View file

@ -6,7 +6,7 @@ import { Component, Input } from '@angular/core';
standalone: true,
imports: [NgClass, NgFor],
templateUrl: './star-rating.component.html',
styleUrl: './star-rating.component.css',
styleUrl: './star-rating.component.css'
})
export class StarRatingComponent {
@Input() public rating: number = 0;

View file

@ -1,15 +0,0 @@
<p>test works!</p>
<form [formGroup]="loginForm">
<label for="username">Username</label>
<input
type="text"
id="username"
autocomplete="username"
formControlName="username"
/>
<label for="password">Password</label>
<input type="password" id="password" formControlName="password" />
<button (click)="submit()" type="submit">Login</button>
<button type="reset" (click)="reset()">Reset</button>
</form>

View file

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

View file

@ -1,34 +0,0 @@
import { Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-test',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './test.component.html',
styleUrl: './test.component.css',
})
export class TestComponent {
public loginForm!: FormGroup;
public ngOnInit(): void {
this.loginForm = this.setUpLoginForm();
console.log(this.loginForm);
}
setUpLoginForm(): FormGroup {
return new FormGroup({
username: new FormControl('Jan'),
password: new FormControl(''),
});
}
reset(): void {
this.loginForm = this.setUpLoginForm();
}
submit(): void {
console.log(this.loginForm.value);
}
}

View file

@ -1,11 +1,11 @@
import { Pipe, PipeTransform } from '@angular/core';
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({
name: 'jkcurrency',
standalone: true,
standalone: true
})
export class CurrencyPipe implements PipeTransform {
transform(value: string): string {
return value.replaceAll('L', 'P');
return value.replaceAll("L", "P");
}
}

View file

@ -1,14 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>HotelManager</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="stylesheet" href="./styles.css" />
</head>
<body class="bg-pink-600 p-32">
<app-root></app-root>
</body>
<head>
<meta charset="utf-8">
<title>HotelManager</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="./styles.css">
<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">
<app-root></app-root>
</body>
</html>

View file

@ -2,6 +2,5 @@ import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err),
);
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));

View file

@ -7,18 +7,5 @@
@apply border-black rounded px-3 py-1 border-[3px];
}
.input-field {
@apply border-red-500 text-3xl rounded-full bg-gray-500 font-bold p-3 m-3 border-8;
}
.back-button {
@apply border-black rounded bg-blue-500 border-[30px] m-3 text-3xl font-bold p-3;
}
.submit-button {
@apply border-black rounded-full bg-green-500 border-[30px] m-3 text-3xl font-bold p-3;
}
.delete-button {
@apply border-black rounded bg-red-500 border-[30px] m-3 text-3xl font-bold p-3;
}
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }

View file

@ -1,11 +1,11 @@
import { Pipe, PipeTransform } from '@angular/core';
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({
name: 'text',
standalone: true,
standalone: true
})
export class TextPipe implements PipeTransform {
transform(value: string): string {
return value.replaceAll('L', 'P');
return value.replaceAll("L", "P");
}
}

View file

@ -1,25 +1,9 @@
/** @type {import('tailwindcss').Config} */
const colors = require("tailwindcss/colors");
module.exports = {
content: ["./src/**/*.{html,ts}"],
theme: {
extend: {
colors: {
...colors,
},
keyframes: {
shake: {
"0%, 100%": { transform: "translateX(0)" },
"25%": { transform: "translateX(-30px)" },
"75%": { transform: "translateX(30px)" },
},
},
animation: {
shake: "shake 10s ease-in-out infinite",
},
},
},
content: [
"./src/**/*.{html,ts}",
],
plugins: [],
};
}

View file

@ -6,6 +6,10 @@
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
"files": [
"src/main.ts"
],
"include": [
"src/**/*.d.ts"
]
}

View file

@ -18,7 +18,10 @@
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022", "dom"]
"lib": [
"ES2022",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,

View file

@ -4,7 +4,12 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": ["jasmine"]
"types": [
"jasmine"
]
},
"include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}