Compare commits
	
		
			5 commits
		
	
	
		
			
				main
			
			...
			
				jleibl_hot
			
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 6d520d4467 | ||
|  | a6829fc3f5 | ||
|  | 6e5eadd3a2 | ||
|  | 074da034c5 | ||
|  | dd4faea712 | 
					 63 changed files with 2977 additions and 4578 deletions
				
			
		
							
								
								
									
										19
									
								
								angular.json
									
										
									
									
									
								
							
							
						
						
									
										19
									
								
								angular.json
									
										
									
									
									
								
							|  | @ -16,7 +16,9 @@ | ||||||
|             "outputPath": "dist/hotel-manager", |             "outputPath": "dist/hotel-manager", | ||||||
|             "index": "src/index.html", |             "index": "src/index.html", | ||||||
|             "browser": "src/main.ts", |             "browser": "src/main.ts", | ||||||
|             "polyfills": ["zone.js"], |             "polyfills": [ | ||||||
|  |               "zone.js" | ||||||
|  |             ], | ||||||
|             "tsConfig": "tsconfig.app.json", |             "tsConfig": "tsconfig.app.json", | ||||||
|             "assets": [ |             "assets": [ | ||||||
|               { |               { | ||||||
|  | @ -25,7 +27,10 @@ | ||||||
|               }, |               }, | ||||||
|               "src/assets" |               "src/assets" | ||||||
|             ], |             ], | ||||||
|             "styles": ["src/styles.css"], |             "styles": [ | ||||||
|  |               "@angular/material/prebuilt-themes/azure-blue.css", | ||||||
|  |               "src/styles.css" | ||||||
|  |             ], | ||||||
|             "scripts": [] |             "scripts": [] | ||||||
|           }, |           }, | ||||||
|           "configurations": { |           "configurations": { | ||||||
|  | @ -70,7 +75,10 @@ | ||||||
|         "test": { |         "test": { | ||||||
|           "builder": "@angular-devkit/build-angular:karma", |           "builder": "@angular-devkit/build-angular:karma", | ||||||
|           "options": { |           "options": { | ||||||
|             "polyfills": ["zone.js", "zone.js/testing"], |             "polyfills": [ | ||||||
|  |               "zone.js", | ||||||
|  |               "zone.js/testing" | ||||||
|  |             ], | ||||||
|             "tsConfig": "tsconfig.spec.json", |             "tsConfig": "tsconfig.spec.json", | ||||||
|             "assets": [ |             "assets": [ | ||||||
|               { |               { | ||||||
|  | @ -78,7 +86,10 @@ | ||||||
|                 "input": "public" |                 "input": "public" | ||||||
|               } |               } | ||||||
|             ], |             ], | ||||||
|             "styles": ["src/styles.css"], |             "styles": [ | ||||||
|  |               "@angular/material/prebuilt-themes/azure-blue.css", | ||||||
|  |               "src/styles.css" | ||||||
|  |             ], | ||||||
|             "scripts": [] |             "scripts": [] | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|  |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								bun.lockb
									
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								bun.lockb
									
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										5928
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										5928
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										28
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										28
									
								
								package.json
									
										
									
									
									
								
							|  | @ -10,23 +10,25 @@ | ||||||
|   }, |   }, | ||||||
|   "private": true, |   "private": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@angular/animations": "^19.0.0", |     "@angular/animations": "^18.2.3", | ||||||
|     "@angular/common": "^19.0.0", |     "@angular/cdk": "^18.2.3", | ||||||
|     "@angular/compiler": "^19.0.0", |     "@angular/common": "^18.2.3", | ||||||
|     "@angular/core": "^19.0.0", |     "@angular/compiler": "^18.2.3", | ||||||
|     "@angular/forms": "^19.0.0", |     "@angular/core": "^18.2.3", | ||||||
|     "@angular/platform-browser": "^19.0.0", |     "@angular/forms": "^18.2.3", | ||||||
|     "@angular/platform-browser-dynamic": "^19.0.0", |     "@angular/material": "^18.2.3", | ||||||
|     "@angular/router": "^19.0.0", |     "@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", |     "angular-in-memory-web-api": "^0.18.0", | ||||||
|     "rxjs": "~7.8.0", |     "rxjs": "~7.8.0", | ||||||
|     "tslib": "^2.3.0", |     "tslib": "^2.3.0", | ||||||
|     "zone.js": "~0.15.0" |     "zone.js": "~0.14.3" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@angular-devkit/build-angular": "^19.0.2", |     "@angular-devkit/build-angular": "^18.2.3", | ||||||
|     "@angular/cli": "^19.0.2", |     "@angular/cli": "^18.2.3", | ||||||
|     "@angular/compiler-cli": "^19.0.0", |     "@angular/compiler-cli": "^18.2.3", | ||||||
|     "@types/jasmine": "~5.1.0", |     "@types/jasmine": "~5.1.0", | ||||||
|     "autoprefixer": "^10.4.20", |     "autoprefixer": "^10.4.20", | ||||||
|     "jasmine-core": "~5.1.0", |     "jasmine-core": "~5.1.0", | ||||||
|  | @ -39,4 +41,4 @@ | ||||||
|     "tailwindcss": "^3.4.10", |     "tailwindcss": "^3.4.10", | ||||||
|     "typescript": "~5.5.2" |     "typescript": "~5.5.2" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | @ -1,4 +1,6 @@ | ||||||
| { | { | ||||||
|   "$schema": "https://docs.renovatebot.com/renovate-schema.json", |   "$schema": "https://docs.renovatebot.com/renovate-schema.json", | ||||||
|   "extends": ["local>Renovate/renovate-config"] |   "extends": [ | ||||||
|  |     "local>Renovate/renovate-config" | ||||||
|  |   ] | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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> |  | ||||||
|  | @ -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'; |  | ||||||
| } |  | ||||||
|  | @ -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> |  | ||||||
|  | @ -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); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -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 |  | ||||||
| > |  | ||||||
|  | @ -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', |  | ||||||
|     }, |  | ||||||
|   ]; |  | ||||||
| } |  | ||||||
|  | @ -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> |  | ||||||
|  | @ -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; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -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'], |  | ||||||
|       }, |  | ||||||
|     ]); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -1,6 +1,2 @@ | ||||||
| <input | <label for="search">Search</label> | ||||||
|   [(ngModel)]="input" | <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"> | ||||||
|   (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" |  | ||||||
| /> |  | ||||||
|  |  | ||||||
|  | @ -1,8 +1,6 @@ | ||||||
| import { CommonModule } from '@angular/common'; | import { Component, Input, Output } from "@angular/core"; | ||||||
| import { Component, NgModule } from '@angular/core'; | import { FormsModule } from "@angular/forms"; | ||||||
| import { FormsModule, NgForm, NgModel } from '@angular/forms'; | import { EventEmitter } from "@angular/core"; | ||||||
| import { Input, Output } from '@angular/core'; |  | ||||||
| import { EventEmitter } from '@angular/core'; |  | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-search', |   selector: 'app-search', | ||||||
|  | @ -11,7 +9,7 @@ import { EventEmitter } from '@angular/core'; | ||||||
|   imports: [FormsModule], |   imports: [FormsModule], | ||||||
| }) | }) | ||||||
| export class SearchComponent { | export class SearchComponent { | ||||||
|   @Input() public input: string = ''; |   @Input() public input: string = ""; | ||||||
|   @Output() inputChange = new EventEmitter<string>(); |   @Output() inputChange = new EventEmitter<string>(); | ||||||
| 
 | 
 | ||||||
|   public update(e: string) { |   public update(e: string) { | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { InMemoryDbService } from 'angular-in-memory-web-api'; | 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 |  * Initial data for in memory web api | ||||||
|  | @ -10,6 +10,7 @@ import { Hotel } from '../HotelItem/hotel'; | ||||||
|  * @implements {InMemoryDbService} |  * @implements {InMemoryDbService} | ||||||
|  */ |  */ | ||||||
| export class HotelData implements InMemoryDbService { | export class HotelData implements InMemoryDbService { | ||||||
|  | 
 | ||||||
|   createDb(): Record<string, Hotel[]> { |   createDb(): Record<string, Hotel[]> { | ||||||
|     const hotels: Hotel[] = [ |     const hotels: Hotel[] = [ | ||||||
|       { |       { | ||||||
|  | @ -19,43 +20,39 @@ export class HotelData implements InMemoryDbService { | ||||||
|         price: 230.5, |         price: 230.5, | ||||||
|         imageUrl: 'assets/img/1.jpg', |         imageUrl: 'assets/img/1.jpg', | ||||||
|         rating: 3.5, |         rating: 3.5, | ||||||
|         tags: ['nouveau'], |         tags: ['nouveau'] | ||||||
|       }, |       }, { | ||||||
|       { |  | ||||||
|         id: 2, |         id: 2, | ||||||
|         hotelName: 'Marakech', |         hotelName: 'Marakech', | ||||||
|         description: 'Profitez de la vue sur les montagnes', |         description: 'Profitez de la vue sur les montagnes', | ||||||
|         price: 145.5, |         price: 145.5, | ||||||
|         imageUrl: 'assets/img/2.jpg', |         imageUrl: 'assets/img/2.jpg', | ||||||
|         rating: 5, |         rating: 5, | ||||||
|         tags: ['nouveau'], |         tags: ['nouveau'] | ||||||
|       }, |       }, { | ||||||
|       { |  | ||||||
|         id: 3, |         id: 3, | ||||||
|         hotelName: 'Abudja new look palace', |         hotelName: 'Abudja new look palace', | ||||||
|         description: 'Séjour complet avec service de voitures', |         description: 'Séjour complet avec service de voitures', | ||||||
|         price: 120.12, |         price: 120.12, | ||||||
|         imageUrl: 'assets/img/3.jpg', |         imageUrl: 'assets/img/3.jpg', | ||||||
|         rating: 4, |         rating: 4, | ||||||
|         tags: ['nouveau'], |         tags: ['nouveau'] | ||||||
|       }, |       }, { | ||||||
|       { |  | ||||||
|         id: 4, |         id: 4, | ||||||
|         hotelName: 'Cape town city', |         hotelName: 'Cape town city', | ||||||
|         description: 'Magnifique cadre pour votre séjour', |         description: 'Magnifique cadre pour votre séjour', | ||||||
|         price: 135.12, |         price: 135.12, | ||||||
|         imageUrl: 'assets/img/4.jpg', |         imageUrl: 'assets/img/4.jpg', | ||||||
|         rating: 2.5, |         rating: 2.5, | ||||||
|         tags: ['nouveau'], |         tags: ['nouveau'] | ||||||
|       }, |       } | ||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|     return { hotels }; |     return { hotels }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   genId(hotels: Hotel[]): number { |   genId(hotels: Hotel[]): number { | ||||||
|     return hotels.length > 0 |     return hotels.length > 0 ? Math.max(...hotels.map(hotel => hotel.id)) + 1 : 1; | ||||||
|       ? Math.max(...hotels.map((hotel) => hotel.id)) + 1 |  | ||||||
|       : 1; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1 @@ | ||||||
| <router-outlet></router-outlet> | <router-outlet></router-outlet> | ||||||
| <!-- <p>ID: {{response.id}}</p> --> |  | ||||||
| <!-- <p>userId: {{response.userId}}</p> --> |  | ||||||
| <!-- <p>title: {{response.title}}</p> --> |  | ||||||
| <!-- <p>completed: {{response.completed}}</p> --> |  | ||||||
|  | @ -24,8 +24,6 @@ describe('AppComponent', () => { | ||||||
|     const fixture = TestBed.createComponent(AppComponent); |     const fixture = TestBed.createComponent(AppComponent); | ||||||
|     fixture.detectChanges(); |     fixture.detectChanges(); | ||||||
|     const compiled = fixture.nativeElement as HTMLElement; |     const compiled = fixture.nativeElement as HTMLElement; | ||||||
|     expect(compiled.querySelector('h1')?.textContent).toContain( |     expect(compiled.querySelector('h1')?.textContent).toContain('Hello, hotel-manager'); | ||||||
|       'Hello, hotel-manager', |  | ||||||
|     ); |  | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,38 +1,16 @@ | ||||||
| import { Component, Injectable } from '@angular/core'; | import { Component, Injectable } from '@angular/core'; | ||||||
| import { HotelItem } from './HotelItem/HotelItem.component'; | import { HotelService } from './service/hotel.service'; | ||||||
| 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 { RouterOutlet } from '@angular/router'; | import { RouterOutlet } from '@angular/router'; | ||||||
| 
 | 
 | ||||||
| @Injectable({ providedIn: 'root' }) | @Injectable({providedIn: "root"}) | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-root', |   selector: 'app-root', | ||||||
|   standalone: true, |   standalone: true, | ||||||
|   imports: [ |   imports: [RouterOutlet], | ||||||
|     RouterOutlet, |  | ||||||
|     NgFor, |  | ||||||
|     NgForOf, |  | ||||||
|     NgIf, |  | ||||||
|     HotelItem, |  | ||||||
|     SearchComponent, |  | ||||||
|     UpperCasePipe, |  | ||||||
|     TextPipe, |  | ||||||
|     AsyncPipe, |  | ||||||
|   ], |  | ||||||
|   templateUrl: './app.component.html', |   templateUrl: './app.component.html', | ||||||
|   providers: [HotelService], |   providers: [HotelService], | ||||||
|   styleUrl: './app.component.css', |   styleUrl: './app.component.css' | ||||||
| }) | }) | ||||||
| export class AppComponent {} | export class AppComponent { | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -1,8 +1,4 @@ | ||||||
| import { | import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; | ||||||
|   ApplicationConfig, |  | ||||||
|   importProvidersFrom, |  | ||||||
|   provideZoneChangeDetection, |  | ||||||
| } from '@angular/core'; |  | ||||||
| import { provideRouter } from '@angular/router'; | import { provideRouter } from '@angular/router'; | ||||||
| import localeDe from '@angular/common/locales/de'; | import localeDe from '@angular/common/locales/de'; | ||||||
| import localeCn from '@angular/common/locales/zh-Hans'; | import localeCn from '@angular/common/locales/zh-Hans'; | ||||||
|  | @ -12,6 +8,7 @@ import { registerLocaleData } from '@angular/common'; | ||||||
| import { provideHttpClient } from '@angular/common/http'; | import { provideHttpClient } from '@angular/common/http'; | ||||||
| import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; | import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; | ||||||
| import { HotelData } from './api/api'; | import { HotelData } from './api/api'; | ||||||
|  | import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; | ||||||
| 
 | 
 | ||||||
| registerLocaleData(localeDe, 'de-DE'); | registerLocaleData(localeDe, 'de-DE'); | ||||||
| registerLocaleData(localeCn, 'cn-CN'); | registerLocaleData(localeCn, 'cn-CN'); | ||||||
|  | @ -20,6 +17,5 @@ export const appConfig: ApplicationConfig = { | ||||||
|     provideZoneChangeDetection({ eventCoalescing: true }), |     provideZoneChangeDetection({ eventCoalescing: true }), | ||||||
|     provideRouter(routes), |     provideRouter(routes), | ||||||
|     provideHttpClient(), |     provideHttpClient(), | ||||||
|     importProvidersFrom(InMemoryWebApiModule.forRoot(HotelData)), |     importProvidersFrom(InMemoryWebApiModule.forRoot(HotelData)), provideAnimationsAsync()] | ||||||
|   ], |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -1,26 +1,27 @@ | ||||||
| import { Routes } from '@angular/router'; | import { Routes } from '@angular/router'; | ||||||
|  | import { HotelDetailsComponent } from './hotel-details/hotel-details.component'; | ||||||
| import { HotelListComponent } from './hotel-list/hotel-list.component'; | import { HotelListComponent } from './hotel-list/hotel-list.component'; | ||||||
| import { TestComponent } from './test/test.component'; | import { HotelFormComponent } from './hotel-form/hotel-form.component'; | ||||||
| import { NewHotelComponent } from './new-hotel/new-hotel.component'; | import { formGuard } from './form.guard'; | ||||||
|  | import { NotFoundComponent } from './not-found/not-found.component'; | ||||||
| 
 | 
 | ||||||
| export const routes: Routes = [ | export const routes: Routes = [ | ||||||
|   { |   { | ||||||
|     path: '', |     path: "", | ||||||
|     component: HotelListComponent, |     component: HotelListComponent, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: 'hotels/:id', |     path: "hotels/:id", | ||||||
|     loadComponent: () => |     component: HotelDetailsComponent, | ||||||
|       import('./hotel-details/hotel-details.component').then( |     canDeactivate: [formGuard], | ||||||
|         (c) => c.HotelDetailsComponent, |  | ||||||
|       ), |  | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: 'testing', |     path: "create-hotel", | ||||||
|     component: TestComponent, |     component: HotelFormComponent, | ||||||
|  |     canDeactivate: [formGuard], | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: 'new', |     path: "**", | ||||||
|     component: NewHotelComponent, |     component: NotFoundComponent, | ||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
|  | @ -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; |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										17
									
								
								src/app/form.guard.spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/app/form.guard.spec.ts
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										15
									
								
								src/app/form.guard.ts
									
										
									
									
									
										Normal 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; | ||||||
|  | }; | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| @if (hotel) { | <div class="container p-4 mx-auto max-w-4xl"> | ||||||
|   <app-hotel-item [hotel]="hotel" [isDetail]="true"></app-hotel-item> |   <div *ngIf="hotel"> | ||||||
|   <app-hotel-form [hotel]="hotel"></app-hotel-form> |     <app-hotel-form [hotel]="hotel" #hotelForm></app-hotel-form> | ||||||
|   <button class="delete-button" (click)="delete()">😠 DELETE</button> |   </div> | ||||||
| } | </div> | ||||||
|  |  | ||||||
|  | @ -8,8 +8,9 @@ describe('HotelDetailsComponent', () => { | ||||||
| 
 | 
 | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     await TestBed.configureTestingModule({ |     await TestBed.configureTestingModule({ | ||||||
|       imports: [HotelDetailsComponent], |       imports: [HotelDetailsComponent] | ||||||
|     }).compileComponents(); |     }) | ||||||
|  |     .compileComponents(); | ||||||
| 
 | 
 | ||||||
|     fixture = TestBed.createComponent(HotelDetailsComponent); |     fixture = TestBed.createComponent(HotelDetailsComponent); | ||||||
|     component = fixture.componentInstance; |     component = fixture.componentInstance; | ||||||
|  |  | ||||||
|  | @ -1,61 +1,39 @@ | ||||||
| import { HttpClient } from '@angular/common/http'; | 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 { ActivatedRoute, Router } from '@angular/router'; | ||||||
| import { Hotel } from '../HotelItem/hotel'; | import { Hotel } from '../hotel-item/hotel'; | ||||||
| import { CurrencyPipe, NgFor, NgForOf, NgIf } from '@angular/common'; | import { NgIf } from '@angular/common'; | ||||||
| import { StarRatingComponent } from '../star-rating/star-rating.component'; |  | ||||||
| import { HotelItem } from '../HotelItem/HotelItem.component'; |  | ||||||
| import { catchError, EMPTY } from 'rxjs'; | import { catchError, EMPTY } from 'rxjs'; | ||||||
| import { HotelFormComponent } from '../hotel-form/hotel-form.component'; | import { HotelFormComponent } from "../hotel-form/hotel-form.component"; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-hotel-details', |   selector: 'app-hotel-details', | ||||||
|   standalone: true, |   standalone: true, | ||||||
|   imports: [ |   imports: [HotelFormComponent, NgIf], | ||||||
|     CurrencyPipe, |  | ||||||
|     StarRatingComponent, |  | ||||||
|     HotelItem, |  | ||||||
|     HotelFormComponent, |  | ||||||
|     NgIf, |  | ||||||
|     NgForOf, |  | ||||||
|   ], |  | ||||||
|   templateUrl: './hotel-details.component.html', |   templateUrl: './hotel-details.component.html', | ||||||
|   styleUrl: './hotel-details.component.css', |   styleUrl: './hotel-details.component.css' | ||||||
| }) | }) | ||||||
| export class HotelDetailsComponent implements OnInit { | export class HotelDetailsComponent implements OnInit { | ||||||
|   public hotel: any; |   public hotel: any; | ||||||
| 
 | 
 | ||||||
|   constructor( |   @ViewChild('hotelForm', { static: false }) | ||||||
|     private route: ActivatedRoute, |   public hotelForm!: HotelFormComponent; | ||||||
|     private http: HttpClient, |  | ||||||
|     private router: Router, |  | ||||||
|   ) {} |  | ||||||
| 
 | 
 | ||||||
|   delete(): void { |   constructor(private route: ActivatedRoute, private http: HttpClient, private router: Router) { } | ||||||
|     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 { |   ngOnInit(): void { | ||||||
|     const routeParams = this.route.snapshot.paramMap; |     const routeParams = this.route.snapshot.paramMap; | ||||||
|     const hotelId = routeParams.get('id'); |     const hotelId = routeParams.get("id"); | ||||||
| 
 | 
 | ||||||
|     this.http |     this.http.get<Hotel>("api/hotels/" + hotelId).pipe( | ||||||
|       .get<Hotel>('api/hotels/' + hotelId) |       catchError(() => { | ||||||
|       .pipe( |         alert("Not Found"); | ||||||
|         catchError(() => { |         this.router.navigate(["/"]); | ||||||
|           alert('Not Found'); |  | ||||||
|           this.router.navigate(['/']); |  | ||||||
| 
 | 
 | ||||||
|           return EMPTY; |         return EMPTY; | ||||||
|         }), |       }) | ||||||
|       ) |     ).subscribe(res => { | ||||||
|       .subscribe((res) => { |       this.hotel = res; | ||||||
|         this.hotel = res; |     }) | ||||||
|       }); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,47 +1,70 @@ | ||||||
| <p>hotel-form works!</p> | <form class="space-y-4 max-w-md mx-auto mt-4" [formGroup]="form"> | ||||||
| <h1>{{ errorMsg }}</h1> |   <ng-container *ngFor="let field of ['name', 'description', 'price', 'rating']"> | ||||||
| <form [formGroup]="hotelForm"> |     <div class="form-group"> | ||||||
|   <label for="name">Name</label> |       <label [for]="'hotel' + field" class="block text-sm font-medium text-gray-700"> | ||||||
|   <div class="text-red-500 text-5xl font-bold" *ngIf="errorMessages['name']"> |         Hotel {{field.charAt(0).toUpperCase() + field.slice(1)}} | ||||||
|     {{ errorMessages["name"] }} |       </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> |   </div> | ||||||
|   <input | 
 | ||||||
|     type="text" |   <div class="flex justify-between"> | ||||||
|     class="border-red-500 text-3xl border-3 rounded-full bg-gray-500 font-bold p-3 m-3 border-8" |     <div class="flex space-x-1"> | ||||||
|     id="name" |       <button type="submit" mat-flat-button (click)="submit()">Submit</button> | ||||||
|     formControlName="name" |       <button type="button" mat-stroked-button routerLink="/">Cancel</button> | ||||||
|   /> |     </div> | ||||||
|   <label for="description">Description</label> |     <button mat-stroked-button color="warn" aria-label="Delete hotel" *ngIf="hotel" (click)="showDeleteConfirmation = true"> | ||||||
|   <div class="text-red" *ngIf="errorMessages['description']"> |       Remove | ||||||
|     {{ errorMessages["description"] }} |     </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> |   </div> | ||||||
|   <input | </form> | ||||||
|     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> |  | ||||||
|  | @ -8,8 +8,9 @@ describe('HotelFormComponent', () => { | ||||||
| 
 | 
 | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     await TestBed.configureTestingModule({ |     await TestBed.configureTestingModule({ | ||||||
|       imports: [HotelFormComponent], |       imports: [HotelFormComponent] | ||||||
|     }).compileComponents(); |     }) | ||||||
|  |     .compileComponents(); | ||||||
| 
 | 
 | ||||||
|     fixture = TestBed.createComponent(HotelFormComponent); |     fixture = TestBed.createComponent(HotelFormComponent); | ||||||
|     component = fixture.componentInstance; |     component = fixture.componentInstance; | ||||||
|  |  | ||||||
|  | @ -1,90 +1,150 @@ | ||||||
| import { Component, Input } from '@angular/core'; | import { Component, inject, Input } from '@angular/core'; | ||||||
| import { Hotel } from '../HotelItem/hotel'; | import { AbstractControl, FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; | ||||||
| import { | import { Hotel } from "../hotel-item/hotel"; | ||||||
|   AbstractControl, |  | ||||||
|   FormControl, |  | ||||||
|   FormGroup, |  | ||||||
|   ReactiveFormsModule, |  | ||||||
|   Validators, |  | ||||||
| } from '@angular/forms'; |  | ||||||
| import { NgFor, NgIf } from '@angular/common'; |  | ||||||
| import { HttpClient } from '@angular/common/http'; | 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({ | @Component({ | ||||||
|   selector: 'app-hotel-form', |   selector: 'app-hotel-form', | ||||||
|   standalone: true, |   standalone: true, | ||||||
|   imports: [ReactiveFormsModule, NgIf, NgFor, RouterLink], |   imports: [ | ||||||
|  |     ReactiveFormsModule, | ||||||
|  |     RouterLink, | ||||||
|  |     NgIf, | ||||||
|  |     MatButtonModule, | ||||||
|  |     MatIconModule, | ||||||
|  |     NgFor, | ||||||
|  |     FormsModule, | ||||||
|  |     NgClass | ||||||
|  |   ], | ||||||
|   templateUrl: './hotel-form.component.html', |   templateUrl: './hotel-form.component.html', | ||||||
|   styleUrl: './hotel-form.component.css', |   styleUrl: './hotel-form.component.css' | ||||||
| }) | }) | ||||||
| export class HotelFormComponent { | export class HotelFormComponent { | ||||||
|   @Input() public hotel!: Hotel; |   public form!: FormGroup; | ||||||
|   public hotelForm!: FormGroup; |  | ||||||
|   public errorMsg: string = 'testing'; |  | ||||||
|   errorMessages: Record<string, string> = {}; |  | ||||||
| 
 | 
 | ||||||
|   constructor( |   @Input() | ||||||
|     public http: HttpClient, |   public hotel!: Hotel; | ||||||
|     public router: Router, |  | ||||||
|   ) {} |  | ||||||
| 
 | 
 | ||||||
|   private validationErrorMessages: Record<string, string> = { |   public router: Router = inject(Router); | ||||||
|     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 { |   public showDeleteConfirmation = false; | ||||||
|     if ((value.touched || value.dirty) && value.errors) { | 
 | ||||||
|       this.errorMsg = Object.keys(value.errors) |   constructor(private http: HttpClient) { } | ||||||
|         .map((key) => { | 
 | ||||||
|           console.log(this.validationErrorMessages[key]); |   public errorMessage: string = ''; | ||||||
|           return this.validationErrorMessages[key] || `Unknown error: ${key}`; | 
 | ||||||
|         }) |   ngOnInit() { | ||||||
|         .join(' '); |     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 { |   private validationErrors: Record<string, string> = { | ||||||
|     this.errorMessages = {}; |     '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) => { |   private setError(control: AbstractControl, controlName: string) { | ||||||
|       const control = this.hotelForm.get(field); |     if ((control.touched || control.dirty) && control.errors) { | ||||||
| 
 |       const errors = Object.keys(control.errors).map(key => { | ||||||
|       if (control && control.errors) { |         const errorKey = `${controlName}.${key}`; | ||||||
|         this.errorMessages[field] = Object.keys(control.errors) |         return this.validationErrors[errorKey] || this.validationErrors[key]; | ||||||
|           .map( |       }); | ||||||
|             (errorKey) => |       this.errorMessage = errors.join(' '); | ||||||
|               this.validationErrorMessages[errorKey] || |     } else { | ||||||
|               `Unknown error: ${errorKey}`, |       this.errorMessage = ''; | ||||||
|           ) |     } | ||||||
|           .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(['/']); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  |  | ||||||
							
								
								
									
										8
									
								
								src/app/hotel-item/HotelItem.component.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/app/hotel-item/HotelItem.component.html
									
										
									
									
									
										Normal 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> | ||||||
							
								
								
									
										53
									
								
								src/app/hotel-item/HotelItem.component.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/app/hotel-item/HotelItem.component.ts
									
										
									
									
									
										Normal 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" | ||||||
|  |     } | ||||||
|  |   ]; | ||||||
|  | } | ||||||
|  | @ -6,6 +6,4 @@ export interface Hotel { | ||||||
|   imageUrl: string; |   imageUrl: string; | ||||||
|   rating: number; |   rating: number; | ||||||
|   tags: Array<string>; |   tags: Array<string>; | ||||||
|   email?: string; |  | ||||||
|   phone?: string; |  | ||||||
| } | } | ||||||
|  | @ -1,15 +1,28 @@ | ||||||
| <div class="container p-4 mx-auto max-w-4xl"> | <div class="container p-4 mx-auto max-w-4xl"> | ||||||
|   <button (click)="loadTest2()">Load Test2</button> |   <div class="fixed top-0 left-0 w-full bg-white/80 backdrop-blur-xl z-50"> | ||||||
|   <ng-container #test2></ng-container> |     <div class="justify-center max-w-4xl container p-4 mx-auto"> | ||||||
|   <h1 class="mt-6">{{ "hello" | uppercase | text }}</h1> |       <h1 class="">{{'hello' | uppercase | text}}</h1> | ||||||
|   <a class="submit-button" routerLink="/new">CREATE NEW HOTEL</a> |       <app-search [(input)]="search"></app-search> | ||||||
|   <app-search [(input)]="search"></app-search> |       <button routerLink="/create-hotel" mat-flat-button | ||||||
|   @if (hotels[0].hotelName) { |         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 | ||||||
|     <div *ngFor="let hotel of hotels"> |         New Hotel</button> | ||||||
|       <app-hotel-item |  | ||||||
|         *ngIf="hotel.hotelName.toLowerCase().includes(search.toLowerCase())" |  | ||||||
|         [hotel]="hotel" |  | ||||||
|       ></app-hotel-item> |  | ||||||
|     </div> |     </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> | </div> | ||||||
|  |  | ||||||
|  | @ -8,8 +8,9 @@ describe('HotelListComponent', () => { | ||||||
| 
 | 
 | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     await TestBed.configureTestingModule({ |     await TestBed.configureTestingModule({ | ||||||
|       imports: [HotelListComponent], |       imports: [HotelListComponent] | ||||||
|     }).compileComponents(); |     }) | ||||||
|  |     .compileComponents(); | ||||||
| 
 | 
 | ||||||
|     fixture = TestBed.createComponent(HotelListComponent); |     fixture = TestBed.createComponent(HotelListComponent); | ||||||
|     component = fixture.componentInstance; |     component = fixture.componentInstance; | ||||||
|  |  | ||||||
|  | @ -1,94 +1,33 @@ | ||||||
| import { CommonModule, NgFor, NgIf, UpperCasePipe } from '@angular/common'; | import { NgFor, NgIf, UpperCasePipe } from '@angular/common'; | ||||||
| import { Component, inject, ViewChild, ViewContainerRef } from '@angular/core'; | import { Component, inject } from '@angular/core'; | ||||||
| import { TextPipe } from '../../text.pipe'; | import { TextPipe } from '../../text.pipe'; | ||||||
| import { SearchComponent } from '../Search/search.component'; | import { SearchComponent } from '../search/search.component'; | ||||||
| import { HotelService } from '../Parent/services/hotel.service'; | import { HotelService } from '../service/hotel.service'; | ||||||
| import { Hotel } from '../HotelItem/hotel'; | import { Hotel } from '../hotel-item/hotel'; | ||||||
| import { HttpClient } from '@angular/common/http'; | import { HttpClient } from '@angular/common/http'; | ||||||
| import { filter, from, last, map, Observable, scan } from 'rxjs'; | import { HotelItem } from '../hotel-item/HotelItem.component'; | ||||||
| import { HotelItem } from '../HotelItem/HotelItem.component'; |  | ||||||
| import { RouterLink } from '@angular/router'; | import { RouterLink } from '@angular/router'; | ||||||
| 
 | import { MatButtonModule } from '@angular/material/button'; | ||||||
| interface User { |  | ||||||
|   name: string; |  | ||||||
|   age: number; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-hotel-list', |   selector: 'app-hotel-list', | ||||||
|   standalone: true, |   standalone: true, | ||||||
|   imports: [ |   imports: [UpperCasePipe, TextPipe, SearchComponent, HotelItem, NgFor, NgIf, RouterLink, MatButtonModule], | ||||||
|     UpperCasePipe, |  | ||||||
|     TextPipe, |  | ||||||
|     SearchComponent, |  | ||||||
|     HotelItem, |  | ||||||
|     NgFor, |  | ||||||
|     NgIf, |  | ||||||
|     RouterLink, |  | ||||||
|   ], |  | ||||||
|   templateUrl: './hotel-list.component.html', |   templateUrl: './hotel-list.component.html', | ||||||
|   styleUrl: './hotel-list.component.css', |   styleUrl: './hotel-list.component.css' | ||||||
| }) | }) | ||||||
| export class HotelListComponent { | export class HotelListComponent { | ||||||
|   public search: string = ''; |   public search: string = ""; | ||||||
|   public hotelService: HotelService = inject(HotelService); |   public hotelService: HotelService = inject(HotelService); | ||||||
|   public response: any = null; |   public response: any = null; | ||||||
|   public hotels: Array<Hotel> = [{} as Hotel]; |   public hotels: Array<Hotel> = [{} as Hotel]; | ||||||
| 
 | 
 | ||||||
|   @ViewChild('test2', { read: ViewContainerRef }) |   constructor (private http: HttpClient) { | ||||||
|   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() { |   ngOnInit() { | ||||||
|     this.http.get<Array<Hotel>>('api/hotels').subscribe((res) => { |     this.http.get<Array<Hotel>>("api/hotels").subscribe(res => { | ||||||
|       this.hotels = 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)),
 |  | ||||||
|   // );
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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> |  | ||||||
|  | @ -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(); |  | ||||||
|   }); |  | ||||||
| }); |  | ||||||
|  | @ -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(['/']); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										7
									
								
								src/app/not-found/not-found.component.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/app/not-found/not-found.component.html
									
										
									
									
									
										Normal 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> | ||||||
|  | @ -1,18 +1,18 @@ | ||||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||||
| 
 | 
 | ||||||
| import { Test2Component } from './test2.component'; | import { NotFoundComponent } from './not-found.component'; | ||||||
| 
 | 
 | ||||||
| describe('Test2Component', () => { | describe('NotFoundComponent', () => { | ||||||
|   let component: Test2Component; |   let component: NotFoundComponent; | ||||||
|   let fixture: ComponentFixture<Test2Component>; |   let fixture: ComponentFixture<NotFoundComponent>; | ||||||
| 
 | 
 | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     await TestBed.configureTestingModule({ |     await TestBed.configureTestingModule({ | ||||||
|       imports: [Test2Component] |       imports: [NotFoundComponent] | ||||||
|     }) |     }) | ||||||
|     .compileComponents(); |     .compileComponents(); | ||||||
| 
 | 
 | ||||||
|     fixture = TestBed.createComponent(Test2Component); |     fixture = TestBed.createComponent(NotFoundComponent); | ||||||
|     component = fixture.componentInstance; |     component = fixture.componentInstance; | ||||||
|     fixture.detectChanges(); |     fixture.detectChanges(); | ||||||
|   }); |   }); | ||||||
							
								
								
									
										17
									
								
								src/app/not-found/not-found.component.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/app/not-found/not-found.component.ts
									
										
									
									
									
										Normal 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 { | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										51
									
								
								src/app/service/hotel.service.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/app/service/hotel.service.ts
									
										
									
									
									
										Normal 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)); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -8,8 +8,9 @@ describe('StarRatingComponent', () => { | ||||||
| 
 | 
 | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     await TestBed.configureTestingModule({ |     await TestBed.configureTestingModule({ | ||||||
|       imports: [StarRatingComponent], |       imports: [StarRatingComponent] | ||||||
|     }).compileComponents(); |     }) | ||||||
|  |     .compileComponents(); | ||||||
| 
 | 
 | ||||||
|     fixture = TestBed.createComponent(StarRatingComponent); |     fixture = TestBed.createComponent(StarRatingComponent); | ||||||
|     component = fixture.componentInstance; |     component = fixture.componentInstance; | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ import { Component, Input } from '@angular/core'; | ||||||
|   standalone: true, |   standalone: true, | ||||||
|   imports: [NgClass, NgFor], |   imports: [NgClass, NgFor], | ||||||
|   templateUrl: './star-rating.component.html', |   templateUrl: './star-rating.component.html', | ||||||
|   styleUrl: './star-rating.component.css', |   styleUrl: './star-rating.component.css' | ||||||
| }) | }) | ||||||
| export class StarRatingComponent { | export class StarRatingComponent { | ||||||
|   @Input() public rating: number = 0; |   @Input() public rating: number = 0; | ||||||
|  |  | ||||||
|  | @ -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> |  | ||||||
|  | @ -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(); |  | ||||||
|   }); |  | ||||||
| }); |  | ||||||
|  | @ -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); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -1,6 +0,0 @@ | ||||||
| <iframe |  | ||||||
|   src="https://funhtml5games.com?embed=flappy" |  | ||||||
|   style="width: 800px; height: 520px; border: none" |  | ||||||
|   frameborder="0" |  | ||||||
|   scrolling="no" |  | ||||||
| ></iframe> |  | ||||||
|  | @ -1,11 +0,0 @@ | ||||||
| import { Component } from '@angular/core'; |  | ||||||
| 
 |  | ||||||
| @Component({ |  | ||||||
|   selector: 'app-test2', |  | ||||||
|   imports: [], |  | ||||||
|   templateUrl: './test2.component.html', |  | ||||||
|   styleUrl: './test2.component.css' |  | ||||||
| }) |  | ||||||
| export class Test2Component { |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| import { Pipe, PipeTransform } from '@angular/core'; | import {Pipe, PipeTransform} from '@angular/core'; | ||||||
| 
 | 
 | ||||||
| @Pipe({ | @Pipe({ | ||||||
|   name: 'jkcurrency', |   name: 'jkcurrency', | ||||||
|   standalone: true, |   standalone: true | ||||||
| }) | }) | ||||||
| export class CurrencyPipe implements PipeTransform { | export class CurrencyPipe implements PipeTransform { | ||||||
|   transform(value: string): string { |   transform(value: string): string { | ||||||
|     return value.replaceAll('L', 'P'); |     return value.replaceAll("L", "P"); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,14 +1,16 @@ | ||||||
| <!doctype html> | <!doctype html> | ||||||
| <html lang="en"> | <html lang="en"> | ||||||
|   <head> | <head> | ||||||
|     <meta charset="utf-8" /> |   <meta charset="utf-8"> | ||||||
|     <title>HotelManager</title> |   <title>HotelManager</title> | ||||||
|     <base href="/" /> |   <base href="/"> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1" /> |   <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
|     <link rel="icon" type="image/x-icon" href="favicon.ico" /> |   <link rel="icon" type="image/x-icon" href="favicon.ico"> | ||||||
|     <link rel="stylesheet" href="./styles.css" /> |   <link rel="stylesheet" href="./styles.css"> | ||||||
|   </head> |   <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> | ||||||
|   <body class="bg-pink-600 p-32"> |   <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> | ||||||
|     <app-root></app-root> | </head> | ||||||
|   </body> | <body class="mat-typography"> | ||||||
|  |   <app-root></app-root> | ||||||
|  | </body> | ||||||
| </html> | </html> | ||||||
|  |  | ||||||
|  | @ -2,6 +2,5 @@ import { bootstrapApplication } from '@angular/platform-browser'; | ||||||
| import { appConfig } from './app/app.config'; | import { appConfig } from './app/app.config'; | ||||||
| import { AppComponent } from './app/app.component'; | import { AppComponent } from './app/app.component'; | ||||||
| 
 | 
 | ||||||
| bootstrapApplication(AppComponent, appConfig).catch((err) => | bootstrapApplication(AppComponent, appConfig) | ||||||
|   console.error(err), |   .catch((err) => console.error(err)); | ||||||
| ); |  | ||||||
|  |  | ||||||
|  | @ -7,18 +7,5 @@ | ||||||
|   @apply border-black rounded px-3 py-1 border-[3px]; |   @apply border-black rounded px-3 py-1 border-[3px]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .input-field { | html, body { height: 100%; } | ||||||
|   @apply border-red-500 text-3xl rounded-full bg-gray-500 font-bold p-3 m-3 border-8; | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .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; |  | ||||||
| } |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| import { Pipe, PipeTransform } from '@angular/core'; | import {Pipe, PipeTransform} from '@angular/core'; | ||||||
| 
 | 
 | ||||||
| @Pipe({ | @Pipe({ | ||||||
|   name: 'text', |   name: 'text', | ||||||
|   standalone: true, |   standalone: true | ||||||
| }) | }) | ||||||
| export class TextPipe implements PipeTransform { | export class TextPipe implements PipeTransform { | ||||||
|   transform(value: string): string { |   transform(value: string): string { | ||||||
|     return value.replaceAll('L', 'P'); |     return value.replaceAll("L", "P"); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,25 +1,9 @@ | ||||||
| /** @type {import('tailwindcss').Config} */ | /** @type {import('tailwindcss').Config} */ | ||||||
| 
 | 
 | ||||||
| const colors = require("tailwindcss/colors"); |  | ||||||
| 
 |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   content: ["./src/**/*.{html,ts}"], |   content: [ | ||||||
|   theme: { |     "./src/**/*.{html,ts}", | ||||||
|     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: [], |   plugins: [], | ||||||
| }; | } | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -6,6 +6,10 @@ | ||||||
|     "outDir": "./out-tsc/app", |     "outDir": "./out-tsc/app", | ||||||
|     "types": [] |     "types": [] | ||||||
|   }, |   }, | ||||||
|   "files": ["src/main.ts"], |   "files": [ | ||||||
|   "include": ["src/**/*.d.ts"] |     "src/main.ts" | ||||||
|  |   ], | ||||||
|  |   "include": [ | ||||||
|  |     "src/**/*.d.ts" | ||||||
|  |   ] | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -18,7 +18,10 @@ | ||||||
|     "importHelpers": true, |     "importHelpers": true, | ||||||
|     "target": "ES2022", |     "target": "ES2022", | ||||||
|     "module": "ES2022", |     "module": "ES2022", | ||||||
|     "lib": ["ES2022", "dom"] |     "lib": [ | ||||||
|  |       "ES2022", | ||||||
|  |       "dom" | ||||||
|  |     ] | ||||||
|   }, |   }, | ||||||
|   "angularCompilerOptions": { |   "angularCompilerOptions": { | ||||||
|     "enableI18nLegacyMessageIdFormat": false, |     "enableI18nLegacyMessageIdFormat": false, | ||||||
|  |  | ||||||
|  | @ -4,7 +4,12 @@ | ||||||
|   "extends": "./tsconfig.json", |   "extends": "./tsconfig.json", | ||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|     "outDir": "./out-tsc/spec", |     "outDir": "./out-tsc/spec", | ||||||
|     "types": ["jasmine"] |     "types": [ | ||||||
|  |       "jasmine" | ||||||
|  |     ] | ||||||
|   }, |   }, | ||||||
|   "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] |   "include": [ | ||||||
|  |     "src/**/*.spec.ts", | ||||||
|  |     "src/**/*.d.ts" | ||||||
|  |   ] | ||||||
| } | } | ||||||
|  |  | ||||||
		Reference in a new issue