Compare commits

...
This repository has been archived on 2025-04-26. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.

14 commits

55 changed files with 4517 additions and 2622 deletions

View file

@ -16,9 +16,7 @@
"outputPath": "dist/hotel-manager",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": [
{
@ -27,9 +25,7 @@
},
"src/assets"
],
"styles": [
"src/styles.css"
],
"styles": ["src/styles.css"],
"scripts": []
},
"configurations": {
@ -74,10 +70,7 @@
"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": [
{
@ -85,9 +78,7 @@
"input": "public"
}
],
"styles": [
"src/styles.css"
],
"styles": ["src/styles.css"],
"scripts": []
}
}

5962
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,23 +10,23 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^18.2.3",
"@angular/common": "^18.2.3",
"@angular/compiler": "^18.2.3",
"@angular/core": "^18.2.3",
"@angular/forms": "^18.2.3",
"@angular/platform-browser": "^18.2.3",
"@angular/platform-browser-dynamic": "^18.2.3",
"@angular/router": "^18.2.3",
"@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-in-memory-web-api": "^0.18.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^18.2.3",
"@angular/cli": "^18.2.3",
"@angular/compiler-cli": "^18.2.3",
"@angular-devkit/build-angular": "^19.0.2",
"@angular/cli": "^19.0.2",
"@angular/compiler-cli": "^19.0.0",
"@types/jasmine": "~5.1.0",
"autoprefixer": "^10.4.20",
"jasmine-core": "~5.1.0",

View file

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

View file

@ -1 +1,6 @@
<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>
<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,5 +1,4 @@
import { Component } from "@angular/core";
import { Component } from '@angular/core';
@Component({
selector: 'app-button',

View file

@ -1,4 +1,12 @@
<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>
<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,4 +1,4 @@
import { Component, Input, Output, EventEmitter } from "@angular/core";
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-child',

View file

@ -1,8 +1,37 @@
<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>
<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 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>
<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,20 +1,27 @@
import { Component, Injectable, Input } from "@angular/core";
import { ChildComponent } from "../Child/child.component";
import { Hotel } from "./hotel";
import { CurrencyPipe, 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";
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],
imports: [
ChildComponent,
CurrencyPipe,
FormsModule,
StarRatingComponent,
NgIf,
RouterLink,
NgFor,
],
})
export class HotelItem {
@Input() public hotel!: Hotel;
public selectedLanguage?: string;
@ -38,19 +45,19 @@ export class HotelItem {
public langs = [
{
"lang": "en",
"code": "en-US",
"currency": "USD"
lang: 'en',
code: 'en-US',
currency: 'USD',
},
{
"lang": "cn",
"code": "cn-CN",
"currency": "CNY"
lang: 'cn',
code: 'cn-CN',
currency: 'CNY',
},
{
"lang": "de",
"code": "de-DE",
"currency": "EUR"
}
lang: 'de',
code: 'de-DE',
currency: 'EUR',
},
];
}

View file

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

View file

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

View file

@ -1,6 +1,5 @@
import { Component } from "@angular/core";
import { ChildComponent } from "../Child/child.component";
import { Component } from '@angular/core';
import { ChildComponent } from '../Child/child.component';
@Component({
selector: 'app-parent',

View file

@ -1,47 +1,47 @@
import { Injectable } from "@angular/core";
import { Hotel } from "../../HotelItem/hotel";
import { from, Observable } from "rxjs";
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: 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: 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: 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"]
}
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 +1,6 @@
<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">
<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"
/>

View file

@ -1,8 +1,8 @@
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 { 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';
@Component({
selector: 'app-search',
@ -11,7 +11,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

@ -10,7 +10,6 @@ import { Hotel } from '../HotelItem/hotel';
* @implements {InMemoryDbService}
*/
export class HotelData implements InMemoryDbService {
createDb(): Record<string, Hotel[]> {
const hotels: Hotel[] = [
{
@ -20,39 +19,43 @@ 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

@ -24,6 +24,8 @@ 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,7 +1,13 @@
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 {
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';
@ -10,15 +16,23 @@ import { HttpClient } from '@angular/common/http';
import { Hotel } from './HotelItem/hotel';
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,
NgFor,
NgForOf,
NgIf,
HotelItem,
SearchComponent,
UpperCasePipe,
TextPipe,
AsyncPipe,
],
templateUrl: './app.component.html',
providers: [HotelService],
styleUrl: './app.component.css'
styleUrl: './app.component.css',
})
export class AppComponent {
}
export class AppComponent {}

View file

@ -1,4 +1,8 @@
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';
@ -16,5 +20,6 @@ export const appConfig: ApplicationConfig = {
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(),
importProvidersFrom(InMemoryWebApiModule.forRoot(HotelData))]
importProvidersFrom(InMemoryWebApiModule.forRoot(HotelData)),
],
};

View file

@ -1,14 +1,26 @@
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';
export const routes: Routes = [
{
path: "",
path: '',
component: HotelListComponent,
},
{
path: "hotels/:id",
component: HotelDetailsComponent,
path: 'hotels/:id',
loadComponent: () =>
import('./hotel-details/hotel-details.component').then(
(c) => c.HotelDetailsComponent,
),
},
{
path: 'testing',
component: TestComponent,
},
{
path: 'new',
component: NewHotelComponent,
},
];

View file

@ -0,0 +1,36 @@
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

@ -1 +1,5 @@
<app-hotel-item [hotel]="hotel" [isDetail]="true"></app-hotel-item>
@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>
}

View file

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

View file

@ -2,36 +2,60 @@ import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Hotel } from '../HotelItem/hotel';
import { CurrencyPipe } from '@angular/common';
import { CurrencyPipe, NgFor, NgForOf, NgIf } from '@angular/common';
import { StarRatingComponent } from '../star-rating/star-rating.component';
import { HotelItem } from '../HotelItem/HotelItem.component';
import { catchError, EMPTY } from 'rxjs';
import { HotelFormComponent } from '../hotel-form/hotel-form.component';
@Component({
selector: 'app-hotel-details',
standalone: true,
imports: [CurrencyPipe, StarRatingComponent, HotelItem],
imports: [
CurrencyPipe,
StarRatingComponent,
HotelItem,
HotelFormComponent,
NgIf,
NgForOf,
],
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) { }
constructor(
private route: ActivatedRoute,
private http: HttpClient,
private router: Router,
) {}
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(['/']);
}
}
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(
this.http
.get<Hotel>('api/hotels/' + hotelId)
.pipe(
catchError(() => {
alert("Not Found");
this.router.navigate(["/"]);
alert('Not Found');
this.router.navigate(['/']);
return EMPTY;
})
).subscribe(res => {
}),
)
.subscribe((res) => {
this.hotel = res;
})
});
}
}

View file

@ -0,0 +1,47 @@
<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"] }}
</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>
<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>

View file

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

View file

@ -0,0 +1,90 @@
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 { HttpClient } from '@angular/common/http';
import { Router, RouterLink } from '@angular/router';
@Component({
selector: 'app-hotel-form',
standalone: true,
imports: [ReactiveFormsModule, NgIf, NgFor, RouterLink],
templateUrl: './hotel-form.component.html',
styleUrl: './hotel-form.component.css',
})
export class HotelFormComponent {
@Input() public hotel!: Hotel;
public hotelForm!: FormGroup;
public errorMsg: string = 'testing';
errorMessages: Record<string, string> = {};
constructor(
public http: HttpClient,
public router: Router,
) {}
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',
};
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(' ');
}
}
updateErrorMessages(): void {
this.errorMessages = {};
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(['/']);
}
}

View file

@ -1,9 +1,15 @@
<div class="container p-4 mx-auto max-w-4xl">
<h1>{{'hello' | uppercase | text}}</h1>
<button (click)="loadTest2()">Load Test2</button>
<ng-container #test2></ng-container>
<h1 class="mt-6">{{ "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>
<app-hotel-item
*ngIf="hotel.hotelName.toLowerCase().includes(search.toLowerCase())"
[hotel]="hotel"
></app-hotel-item>
</div>
}
</div>

View file

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

View file

@ -1,5 +1,5 @@
import { CommonModule, NgFor, NgIf, UpperCasePipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import { Component, inject, ViewChild, ViewContainerRef } from '@angular/core';
import { TextPipe } from '../../text.pipe';
import { SearchComponent } from '../Search/search.component';
import { HotelService } from '../Parent/services/hotel.service';
@ -7,6 +7,7 @@ import { Hotel } from '../HotelItem/hotel';
import { HttpClient } from '@angular/common/http';
import { filter, from, last, map, Observable, scan } from 'rxjs';
import { HotelItem } from '../HotelItem/HotelItem.component';
import { RouterLink } from '@angular/router';
interface User {
name: string;
@ -16,41 +17,61 @@ interface User {
@Component({
selector: 'app-hotel-list',
standalone: true,
imports: [UpperCasePipe, TextPipe, SearchComponent, HotelItem, NgFor, NgIf],
imports: [
UpperCasePipe,
TextPipe,
SearchComponent,
HotelItem,
NgFor,
NgIf,
RouterLink,
],
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) {
@ViewChild('test2', { read: ViewContainerRef })
private test2ViewContainerRef!: ViewContainerRef;
constructor(private http: HttpClient) { }
async loadTest2() {
for (let i = 0; i < 1; i++) {
const { Test2Component } = await import('../test2/test2.component');
this.test2ViewContainerRef.createComponent(Test2Component);
}
}
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 },
]
{ 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(
stream
.pipe(
filter((user) => user.age > 18),
scan((acc, user) => acc + user.age, 0),
map((ageSum, index) => ageSum / (index + 1)),
last(),
).subscribe(console.log);
)
.subscribe(console.log);
// const stream: Observable<number | string> = from([5, 1, 2, 12, 5, 14, 17, 5, "testing"]);

View file

@ -0,0 +1,120 @@
<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

@ -0,0 +1,22 @@
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

@ -0,0 +1,139 @@
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

@ -8,9 +8,8 @@ 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

View file

@ -0,0 +1,15 @@
<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

@ -0,0 +1,22 @@
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

@ -0,0 +1,34 @@
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

View file

@ -0,0 +1,6 @@
<iframe
src="https://funhtml5games.com?embed=flappy"
style="width: 800px; height: 520px; border: none"
frameborder="0"
scrolling="no"
></iframe>

View file

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

View file

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-test2',
imports: [],
templateUrl: './test2.component.html',
styleUrl: './test2.component.css'
})
export class Test2Component {
}

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,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<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>
<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>
</body>
</html>

View file

@ -2,5 +2,6 @@ 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

@ -6,3 +6,19 @@
.button {
@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;
}

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,18 +1,25 @@
/** @type {import('tailwindcss').Config} */
const colors = require('tailwindcss/colors');
const colors = require("tailwindcss/colors");
module.exports = {
content: [
"./src/**/*.{html,ts}",
],
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",
},
},
},
plugins: [],
}
};

View file

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

View file

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

View file

@ -4,12 +4,7 @@
"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"]
}