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", | ||||
|             "index": "src/index.html", | ||||
|             "browser": "src/main.ts", | ||||
|             "polyfills": ["zone.js"], | ||||
|             "polyfills": [ | ||||
|               "zone.js" | ||||
|             ], | ||||
|             "tsConfig": "tsconfig.app.json", | ||||
|             "assets": [ | ||||
|               { | ||||
|  | @ -25,7 +27,10 @@ | |||
|               }, | ||||
|               "src/assets" | ||||
|             ], | ||||
|             "styles": ["src/styles.css"], | ||||
|             "styles": [ | ||||
|               "@angular/material/prebuilt-themes/azure-blue.css", | ||||
|               "src/styles.css" | ||||
|             ], | ||||
|             "scripts": [] | ||||
|           }, | ||||
|           "configurations": { | ||||
|  | @ -70,7 +75,10 @@ | |||
|         "test": { | ||||
|           "builder": "@angular-devkit/build-angular:karma", | ||||
|           "options": { | ||||
|             "polyfills": ["zone.js", "zone.js/testing"], | ||||
|             "polyfills": [ | ||||
|               "zone.js", | ||||
|               "zone.js/testing" | ||||
|             ], | ||||
|             "tsConfig": "tsconfig.spec.json", | ||||
|             "assets": [ | ||||
|               { | ||||
|  | @ -78,7 +86,10 @@ | |||
|                 "input": "public" | ||||
|               } | ||||
|             ], | ||||
|             "styles": ["src/styles.css"], | ||||
|             "styles": [ | ||||
|               "@angular/material/prebuilt-themes/azure-blue.css", | ||||
|               "src/styles.css" | ||||
|             ], | ||||
|             "scripts": [] | ||||
|           } | ||||
|         } | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								bun.lockb
									
										
									
									
									
								
							
							
						
						
									
										
											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, | ||||
|   "dependencies": { | ||||
|     "@angular/animations": "^19.0.0", | ||||
|     "@angular/common": "^19.0.0", | ||||
|     "@angular/compiler": "^19.0.0", | ||||
|     "@angular/core": "^19.0.0", | ||||
|     "@angular/forms": "^19.0.0", | ||||
|     "@angular/platform-browser": "^19.0.0", | ||||
|     "@angular/platform-browser-dynamic": "^19.0.0", | ||||
|     "@angular/router": "^19.0.0", | ||||
|     "@angular/animations": "^18.2.3", | ||||
|     "@angular/cdk": "^18.2.3", | ||||
|     "@angular/common": "^18.2.3", | ||||
|     "@angular/compiler": "^18.2.3", | ||||
|     "@angular/core": "^18.2.3", | ||||
|     "@angular/forms": "^18.2.3", | ||||
|     "@angular/material": "^18.2.3", | ||||
|     "@angular/platform-browser": "^18.2.3", | ||||
|     "@angular/platform-browser-dynamic": "^18.2.3", | ||||
|     "@angular/router": "^18.2.3", | ||||
|     "angular-in-memory-web-api": "^0.18.0", | ||||
|     "rxjs": "~7.8.0", | ||||
|     "tslib": "^2.3.0", | ||||
|     "zone.js": "~0.15.0" | ||||
|     "zone.js": "~0.14.3" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@angular-devkit/build-angular": "^19.0.2", | ||||
|     "@angular/cli": "^19.0.2", | ||||
|     "@angular/compiler-cli": "^19.0.0", | ||||
|     "@angular-devkit/build-angular": "^18.2.3", | ||||
|     "@angular/cli": "^18.2.3", | ||||
|     "@angular/compiler-cli": "^18.2.3", | ||||
|     "@types/jasmine": "~5.1.0", | ||||
|     "autoprefixer": "^10.4.20", | ||||
|     "jasmine-core": "~5.1.0", | ||||
|  | @ -39,4 +41,4 @@ | |||
|     "tailwindcss": "^3.4.10", | ||||
|     "typescript": "~5.5.2" | ||||
|   } | ||||
| } | ||||
| } | ||||
|  | @ -1,4 +1,6 @@ | |||
| { | ||||
|   "$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 | ||||
|   [(ngModel)]="input" | ||||
|   (ngModelChange)="update($event)" | ||||
|   class="border border-gray-300 rounded-md p-2 w-full focus:border-blue-500 focus:ring focus:ring-blue-200" | ||||
|   type="search" | ||||
| /> | ||||
| <label for="search">Search</label> | ||||
| <input [(ngModel)]="input" (ngModelChange)="update($event)" id="search" class="border border-gray-300 rounded-md p-2 w-full focus:border-blue-500 focus:ring focus:ring-blue-200" type="search"> | ||||
|  |  | |||
|  | @ -1,8 +1,6 @@ | |||
| import { CommonModule } from '@angular/common'; | ||||
| import { Component, NgModule } from '@angular/core'; | ||||
| import { FormsModule, NgForm, NgModel } from '@angular/forms'; | ||||
| import { Input, Output } from '@angular/core'; | ||||
| import { EventEmitter } from '@angular/core'; | ||||
| import { Component, Input, Output } from "@angular/core"; | ||||
| import { FormsModule } from "@angular/forms"; | ||||
| import { EventEmitter } from "@angular/core"; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-search', | ||||
|  | @ -11,7 +9,7 @@ import { EventEmitter } from '@angular/core'; | |||
|   imports: [FormsModule], | ||||
| }) | ||||
| export class SearchComponent { | ||||
|   @Input() public input: string = ''; | ||||
|   @Input() public input: string = ""; | ||||
|   @Output() inputChange = new EventEmitter<string>(); | ||||
| 
 | ||||
|   public update(e: string) { | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { InMemoryDbService } from 'angular-in-memory-web-api'; | ||||
| 
 | ||||
| import { Hotel } from '../HotelItem/hotel'; | ||||
| import { Hotel } from '../hotel-item/hotel'; | ||||
| 
 | ||||
| /** | ||||
|  * Initial data for in memory web api | ||||
|  | @ -10,6 +10,7 @@ import { Hotel } from '../HotelItem/hotel'; | |||
|  * @implements {InMemoryDbService} | ||||
|  */ | ||||
| export class HotelData implements InMemoryDbService { | ||||
| 
 | ||||
|   createDb(): Record<string, Hotel[]> { | ||||
|     const hotels: Hotel[] = [ | ||||
|       { | ||||
|  | @ -19,43 +20,39 @@ export class HotelData implements InMemoryDbService { | |||
|         price: 230.5, | ||||
|         imageUrl: 'assets/img/1.jpg', | ||||
|         rating: 3.5, | ||||
|         tags: ['nouveau'], | ||||
|       }, | ||||
|       { | ||||
|         tags: ['nouveau'] | ||||
|       }, { | ||||
|         id: 2, | ||||
|         hotelName: 'Marakech', | ||||
|         description: 'Profitez de la vue sur les montagnes', | ||||
|         price: 145.5, | ||||
|         imageUrl: 'assets/img/2.jpg', | ||||
|         rating: 5, | ||||
|         tags: ['nouveau'], | ||||
|       }, | ||||
|       { | ||||
|         tags: ['nouveau'] | ||||
|       }, { | ||||
|         id: 3, | ||||
|         hotelName: 'Abudja new look palace', | ||||
|         description: 'Séjour complet avec service de voitures', | ||||
|         price: 120.12, | ||||
|         imageUrl: 'assets/img/3.jpg', | ||||
|         rating: 4, | ||||
|         tags: ['nouveau'], | ||||
|       }, | ||||
|       { | ||||
|         tags: ['nouveau'] | ||||
|       }, { | ||||
|         id: 4, | ||||
|         hotelName: 'Cape town city', | ||||
|         description: 'Magnifique cadre pour votre séjour', | ||||
|         price: 135.12, | ||||
|         imageUrl: 'assets/img/4.jpg', | ||||
|         rating: 2.5, | ||||
|         tags: ['nouveau'], | ||||
|       }, | ||||
|         tags: ['nouveau'] | ||||
|       } | ||||
|     ]; | ||||
| 
 | ||||
|     return { hotels }; | ||||
|   } | ||||
| 
 | ||||
|   genId(hotels: Hotel[]): number { | ||||
|     return hotels.length > 0 | ||||
|       ? Math.max(...hotels.map((hotel) => hotel.id)) + 1 | ||||
|       : 1; | ||||
|     return hotels.length > 0 ? Math.max(...hotels.map(hotel => hotel.id)) + 1 : 1; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1 @@ | |||
| <router-outlet></router-outlet> | ||||
| <!-- <p>ID: {{response.id}}</p> --> | ||||
| <!-- <p>userId: {{response.userId}}</p> --> | ||||
| <!-- <p>title: {{response.title}}</p> --> | ||||
| <!-- <p>completed: {{response.completed}}</p> --> | ||||
| <router-outlet></router-outlet> | ||||
|  | @ -24,8 +24,6 @@ describe('AppComponent', () => { | |||
|     const fixture = TestBed.createComponent(AppComponent); | ||||
|     fixture.detectChanges(); | ||||
|     const compiled = fixture.nativeElement as HTMLElement; | ||||
|     expect(compiled.querySelector('h1')?.textContent).toContain( | ||||
|       'Hello, hotel-manager', | ||||
|     ); | ||||
|     expect(compiled.querySelector('h1')?.textContent).toContain('Hello, hotel-manager'); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -1,38 +1,16 @@ | |||
| import { Component, Injectable } from '@angular/core'; | ||||
| import { HotelItem } from './HotelItem/HotelItem.component'; | ||||
| import { SearchComponent } from './Search/search.component'; | ||||
| import { | ||||
|   AsyncPipe, | ||||
|   NgFor, | ||||
|   NgForOf, | ||||
|   NgIf, | ||||
|   UpperCasePipe, | ||||
| } from '@angular/common'; | ||||
| import { TextPipe } from '../text.pipe'; | ||||
| import { HotelService } from './Parent/services/hotel.service'; | ||||
| import { inject } from '@angular/core'; | ||||
| import { filter, from, last, map, Observable, scan } from 'rxjs'; | ||||
| import { HttpClient } from '@angular/common/http'; | ||||
| import { Hotel } from './HotelItem/hotel'; | ||||
| import { HotelService } from './service/hotel.service'; | ||||
| import { RouterOutlet } from '@angular/router'; | ||||
| 
 | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| @Injectable({providedIn: "root"}) | ||||
| @Component({ | ||||
|   selector: 'app-root', | ||||
|   standalone: true, | ||||
|   imports: [ | ||||
|     RouterOutlet, | ||||
|     NgFor, | ||||
|     NgForOf, | ||||
|     NgIf, | ||||
|     HotelItem, | ||||
|     SearchComponent, | ||||
|     UpperCasePipe, | ||||
|     TextPipe, | ||||
|     AsyncPipe, | ||||
|   ], | ||||
|   imports: [RouterOutlet], | ||||
|   templateUrl: './app.component.html', | ||||
|   providers: [HotelService], | ||||
|   styleUrl: './app.component.css', | ||||
|   styleUrl: './app.component.css' | ||||
| }) | ||||
| export class AppComponent {} | ||||
| export class AppComponent { | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,8 +1,4 @@ | |||
| import { | ||||
|   ApplicationConfig, | ||||
|   importProvidersFrom, | ||||
|   provideZoneChangeDetection, | ||||
| } from '@angular/core'; | ||||
| import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; | ||||
| import { provideRouter } from '@angular/router'; | ||||
| import localeDe from '@angular/common/locales/de'; | ||||
| import localeCn from '@angular/common/locales/zh-Hans'; | ||||
|  | @ -12,6 +8,7 @@ import { registerLocaleData } from '@angular/common'; | |||
| import { provideHttpClient } from '@angular/common/http'; | ||||
| import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; | ||||
| import { HotelData } from './api/api'; | ||||
| import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; | ||||
| 
 | ||||
| registerLocaleData(localeDe, 'de-DE'); | ||||
| registerLocaleData(localeCn, 'cn-CN'); | ||||
|  | @ -20,6 +17,5 @@ export const appConfig: ApplicationConfig = { | |||
|     provideZoneChangeDetection({ eventCoalescing: true }), | ||||
|     provideRouter(routes), | ||||
|     provideHttpClient(), | ||||
|     importProvidersFrom(InMemoryWebApiModule.forRoot(HotelData)), | ||||
|   ], | ||||
|     importProvidersFrom(InMemoryWebApiModule.forRoot(HotelData)), provideAnimationsAsync()] | ||||
| }; | ||||
|  |  | |||
|  | @ -1,26 +1,27 @@ | |||
| import { Routes } from '@angular/router'; | ||||
| import { HotelDetailsComponent } from './hotel-details/hotel-details.component'; | ||||
| import { HotelListComponent } from './hotel-list/hotel-list.component'; | ||||
| import { TestComponent } from './test/test.component'; | ||||
| import { NewHotelComponent } from './new-hotel/new-hotel.component'; | ||||
| import { HotelFormComponent } from './hotel-form/hotel-form.component'; | ||||
| import { formGuard } from './form.guard'; | ||||
| import { NotFoundComponent } from './not-found/not-found.component'; | ||||
| 
 | ||||
| export const routes: Routes = [ | ||||
|   { | ||||
|     path: '', | ||||
|     path: "", | ||||
|     component: HotelListComponent, | ||||
|   }, | ||||
|   { | ||||
|     path: 'hotels/:id', | ||||
|     loadComponent: () => | ||||
|       import('./hotel-details/hotel-details.component').then( | ||||
|         (c) => c.HotelDetailsComponent, | ||||
|       ), | ||||
|     path: "hotels/:id", | ||||
|     component: HotelDetailsComponent, | ||||
|     canDeactivate: [formGuard], | ||||
|   }, | ||||
|   { | ||||
|     path: 'testing', | ||||
|     component: TestComponent, | ||||
|     path: "create-hotel", | ||||
|     component: HotelFormComponent, | ||||
|     canDeactivate: [formGuard], | ||||
|   }, | ||||
|   { | ||||
|     path: 'new', | ||||
|     component: NewHotelComponent, | ||||
|     path: "**", | ||||
|     component: NotFoundComponent, | ||||
|   }, | ||||
| ]; | ||||
|  |  | |||
|  | @ -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) { | ||||
|   <app-hotel-item [hotel]="hotel" [isDetail]="true"></app-hotel-item> | ||||
|   <app-hotel-form [hotel]="hotel"></app-hotel-form> | ||||
|   <button class="delete-button" (click)="delete()">😠 DELETE</button> | ||||
| } | ||||
| <div class="container p-4 mx-auto max-w-4xl"> | ||||
|   <div *ngIf="hotel"> | ||||
|     <app-hotel-form [hotel]="hotel" #hotelForm></app-hotel-form> | ||||
|   </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -8,8 +8,9 @@ describe('HotelDetailsComponent', () => { | |||
| 
 | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       imports: [HotelDetailsComponent], | ||||
|     }).compileComponents(); | ||||
|       imports: [HotelDetailsComponent] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
| 
 | ||||
|     fixture = TestBed.createComponent(HotelDetailsComponent); | ||||
|     component = fixture.componentInstance; | ||||
|  |  | |||
|  | @ -1,61 +1,39 @@ | |||
| import { HttpClient } from '@angular/common/http'; | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { Component, OnInit, ViewChild } from '@angular/core'; | ||||
| import { ActivatedRoute, Router } from '@angular/router'; | ||||
| import { Hotel } from '../HotelItem/hotel'; | ||||
| import { CurrencyPipe, NgFor, NgForOf, NgIf } from '@angular/common'; | ||||
| import { StarRatingComponent } from '../star-rating/star-rating.component'; | ||||
| import { HotelItem } from '../HotelItem/HotelItem.component'; | ||||
| import { Hotel } from '../hotel-item/hotel'; | ||||
| import { NgIf } from '@angular/common'; | ||||
| import { catchError, EMPTY } from 'rxjs'; | ||||
| import { HotelFormComponent } from '../hotel-form/hotel-form.component'; | ||||
| import { HotelFormComponent } from "../hotel-form/hotel-form.component"; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-hotel-details', | ||||
|   standalone: true, | ||||
|   imports: [ | ||||
|     CurrencyPipe, | ||||
|     StarRatingComponent, | ||||
|     HotelItem, | ||||
|     HotelFormComponent, | ||||
|     NgIf, | ||||
|     NgForOf, | ||||
|   ], | ||||
|   imports: [HotelFormComponent, NgIf], | ||||
|   templateUrl: './hotel-details.component.html', | ||||
|   styleUrl: './hotel-details.component.css', | ||||
|   styleUrl: './hotel-details.component.css' | ||||
| }) | ||||
| export class HotelDetailsComponent implements OnInit { | ||||
|   public hotel: any; | ||||
| 
 | ||||
|   constructor( | ||||
|     private route: ActivatedRoute, | ||||
|     private http: HttpClient, | ||||
|     private router: Router, | ||||
|   ) {} | ||||
|   @ViewChild('hotelForm', { static: false }) | ||||
|   public hotelForm!: HotelFormComponent; | ||||
| 
 | ||||
|   delete(): void { | ||||
|     if (confirm('Are u sure u want to delete this hotel')) { | ||||
|       this.http | ||||
|         .delete<Hotel>('/api/hotels/' + this.route.snapshot.paramMap.get('id')) | ||||
|         .subscribe(); | ||||
|       this.router.navigate(['/']); | ||||
|     } | ||||
|   } | ||||
|   constructor(private route: ActivatedRoute, private http: HttpClient, private router: Router) { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     const routeParams = this.route.snapshot.paramMap; | ||||
|     const hotelId = routeParams.get('id'); | ||||
|     const hotelId = routeParams.get("id"); | ||||
| 
 | ||||
|     this.http | ||||
|       .get<Hotel>('api/hotels/' + hotelId) | ||||
|       .pipe( | ||||
|         catchError(() => { | ||||
|           alert('Not Found'); | ||||
|           this.router.navigate(['/']); | ||||
|     this.http.get<Hotel>("api/hotels/" + hotelId).pipe( | ||||
|       catchError(() => { | ||||
|         alert("Not Found"); | ||||
|         this.router.navigate(["/"]); | ||||
| 
 | ||||
|           return EMPTY; | ||||
|         }), | ||||
|       ) | ||||
|       .subscribe((res) => { | ||||
|         this.hotel = res; | ||||
|       }); | ||||
|         return EMPTY; | ||||
|       }) | ||||
|     ).subscribe(res => { | ||||
|       this.hotel = res; | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,47 +1,70 @@ | |||
| <p>hotel-form works!</p> | ||||
| <h1>{{ errorMsg }}</h1> | ||||
| <form [formGroup]="hotelForm"> | ||||
|   <label for="name">Name</label> | ||||
|   <div class="text-red-500 text-5xl font-bold" *ngIf="errorMessages['name']"> | ||||
|     {{ errorMessages["name"] }} | ||||
| <form class="space-y-4 max-w-md mx-auto mt-4" [formGroup]="form"> | ||||
|   <ng-container *ngFor="let field of ['name', 'description', 'price', 'rating']"> | ||||
|     <div class="form-group"> | ||||
|       <label [for]="'hotel' + field" class="block text-sm font-medium text-gray-700"> | ||||
|         Hotel {{field.charAt(0).toUpperCase() + field.slice(1)}} | ||||
|       </label> | ||||
|       <input [type]="field === 'price' || field === 'rating' ? 'number' : 'text'" [formControlName]="field" | ||||
|         class="form-control mt-1 block w-full p-2 border rounded-md shadow-sm" | ||||
|         [ngClass]="{ | ||||
|           'border-red-500': errorMessage.includes(field), | ||||
|           'border-gray-300': !errorMessage.includes(field), | ||||
|           'focus:border-red-500': errorMessage.includes(field), | ||||
|           'focus:border-blue-500': !errorMessage.includes(field), | ||||
|           'focus:ring-red-200': errorMessage.includes(field), | ||||
|           'focus:ring-blue-200': !errorMessage.includes(field) | ||||
|         }" | ||||
|         [id]="'hotel' + field" [placeholder]="'Enter hotel ' + field"> | ||||
|       <div *ngIf="errorMessage?.includes(field)" class="text-red-500 text-sm mt-1"> | ||||
|         {{errorMessage}} | ||||
|       </div> | ||||
|     </div> | ||||
|   </ng-container> | ||||
| 
 | ||||
|   <div class="form-group"> | ||||
|     <label for="hotelTags" class="block text-sm font-medium text-gray-700">Hotel Tags</label> | ||||
|     <div formArrayName="tags"> | ||||
|       <div *ngFor="let tag of tags.controls; let i=index" class="flex items-center space-x-2"> | ||||
|         <input type="text" [formControlName]="i" | ||||
|           class="form-control mt-1 w-full p-2 border rounded-md shadow-sm" | ||||
|           [ngClass]="{ | ||||
|             'border-red-500': errorMessage.includes('tags'), | ||||
|             'border-gray-300': !errorMessage.includes('tags'), | ||||
|             'focus:border-red-500': errorMessage.includes('tags'), | ||||
|             'focus:border-blue-500': !errorMessage.includes('tags'), | ||||
|             'focus:ring-red-200': errorMessage.includes('tags'), | ||||
|             'focus:ring-blue-200': !errorMessage.includes('tags') | ||||
|           }" | ||||
|           [id]="'hotelTag' + i" placeholder="Enter hotel tag"> | ||||
|         <button mat-icon-button (click)="tags.removeAt(i)" class="mt-1"> | ||||
|           <mat-icon class="text-red-500">delete</mat-icon> | ||||
|         </button> | ||||
|       </div> | ||||
|       <button type="button" mat-button (click)="addTag()" class="mt-2">Add Tag</button> | ||||
|     </div> | ||||
|     <div *ngIf="errorMessage.includes('tags')" class="text-red-500 text-sm mt-1"> | ||||
|       {{errorMessage}} | ||||
|     </div> | ||||
|   </div> | ||||
|   <input | ||||
|     type="text" | ||||
|     class="border-red-500 text-3xl border-3 rounded-full bg-gray-500 font-bold p-3 m-3 border-8" | ||||
|     id="name" | ||||
|     formControlName="name" | ||||
|   /> | ||||
|   <label for="description">Description</label> | ||||
|   <div class="text-red" *ngIf="errorMessages['description']"> | ||||
|     {{ errorMessages["description"] }} | ||||
| 
 | ||||
|   <div class="flex justify-between"> | ||||
|     <div class="flex space-x-1"> | ||||
|       <button type="submit" mat-flat-button (click)="submit()">Submit</button> | ||||
|       <button type="button" mat-stroked-button routerLink="/">Cancel</button> | ||||
|     </div> | ||||
|     <button mat-stroked-button color="warn" aria-label="Delete hotel" *ngIf="hotel" (click)="showDeleteConfirmation = true"> | ||||
|       Remove | ||||
|     </button> | ||||
| 
 | ||||
|     <div *ngIf="showDeleteConfirmation" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> | ||||
|       <div class="bg-white p-6 rounded-lg shadow-lg max-w-sm w-full"> | ||||
|         <h2 class="text-xl font-bold mb-4">Confirm Deletion</h2> | ||||
|         <p class="mb-6">Are you sure you want to delete this hotel?</p> | ||||
|         <div class="flex justify-end space-x-2"> | ||||
|           <button mat-stroked-button (click)="showDeleteConfirmation = false">Cancel</button> | ||||
|           <button mat-flat-button color="warn" (click)="delete()">Delete</button> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   <input | ||||
|     type="text" | ||||
|     class="input-field" | ||||
|     [class.border-8]="hotelForm.get('description')?.invalid" | ||||
|     id="description" | ||||
|     formControlName="description" | ||||
|   /> | ||||
|   <label for="price">Price</label> | ||||
|   <div class="text-red" *ngIf="errorMessages['price']"> | ||||
|     {{ errorMessages["price"] }} | ||||
|   </div> | ||||
|   <input | ||||
|     type="number" | ||||
|     class="input-field" | ||||
|     [class.border-8]=" | ||||
|       hotelForm.get('price')?.invalid && hotelForm.get('price')?.touched | ||||
|     " | ||||
|     id="price" | ||||
|     formControlName="price" | ||||
|   /> | ||||
|   <button | ||||
|     class="submit-button" | ||||
|     (click)="submit()" | ||||
|     [disabled]="!hotelForm.valid" | ||||
|     type="submit" | ||||
|   > | ||||
|     😃 Submit | ||||
|   </button> | ||||
|   <a class="back-button" routerLink="/">😭 Cancel</a> | ||||
| </form> | ||||
| </form> | ||||
|  | @ -8,8 +8,9 @@ describe('HotelFormComponent', () => { | |||
| 
 | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       imports: [HotelFormComponent], | ||||
|     }).compileComponents(); | ||||
|       imports: [HotelFormComponent] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
| 
 | ||||
|     fixture = TestBed.createComponent(HotelFormComponent); | ||||
|     component = fixture.componentInstance; | ||||
|  |  | |||
|  | @ -1,90 +1,150 @@ | |||
| import { Component, Input } from '@angular/core'; | ||||
| import { Hotel } from '../HotelItem/hotel'; | ||||
| import { | ||||
|   AbstractControl, | ||||
|   FormControl, | ||||
|   FormGroup, | ||||
|   ReactiveFormsModule, | ||||
|   Validators, | ||||
| } from '@angular/forms'; | ||||
| import { NgFor, NgIf } from '@angular/common'; | ||||
| import { Component, inject, Input } from '@angular/core'; | ||||
| import { AbstractControl, FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; | ||||
| import { Hotel } from "../hotel-item/hotel"; | ||||
| import { HttpClient } from '@angular/common/http'; | ||||
| import { Router, RouterLink } from '@angular/router'; | ||||
| import { catchError, debounceTime } from "rxjs"; | ||||
| import { Router, RouterLink } from "@angular/router"; | ||||
| import { NgClass, NgFor, NgIf } from "@angular/common"; | ||||
| import { MatButtonModule } from "@angular/material/button"; | ||||
| import { MatIconModule } from "@angular/material/icon"; | ||||
| import { FormsModule } from '@angular/forms'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-hotel-form', | ||||
|   standalone: true, | ||||
|   imports: [ReactiveFormsModule, NgIf, NgFor, RouterLink], | ||||
|   imports: [ | ||||
|     ReactiveFormsModule, | ||||
|     RouterLink, | ||||
|     NgIf, | ||||
|     MatButtonModule, | ||||
|     MatIconModule, | ||||
|     NgFor, | ||||
|     FormsModule, | ||||
|     NgClass | ||||
|   ], | ||||
|   templateUrl: './hotel-form.component.html', | ||||
|   styleUrl: './hotel-form.component.css', | ||||
|   styleUrl: './hotel-form.component.css' | ||||
| }) | ||||
| export class HotelFormComponent { | ||||
|   @Input() public hotel!: Hotel; | ||||
|   public hotelForm!: FormGroup; | ||||
|   public errorMsg: string = 'testing'; | ||||
|   errorMessages: Record<string, string> = {}; | ||||
|   public form!: FormGroup; | ||||
| 
 | ||||
|   constructor( | ||||
|     public http: HttpClient, | ||||
|     public router: Router, | ||||
|   ) {} | ||||
|   @Input() | ||||
|   public hotel!: Hotel; | ||||
| 
 | ||||
|   private validationErrorMessages: Record<string, string> = { | ||||
|     required: 'This field is required', | ||||
|     minlength: 'The value is too short', | ||||
|     maxlength: 'The value is too long', | ||||
|     pattern: 'The value format is incorrect', | ||||
|   }; | ||||
|   public router: Router = inject(Router); | ||||
| 
 | ||||
|   setMessage(value: AbstractControl): void { | ||||
|     if ((value.touched || value.dirty) && value.errors) { | ||||
|       this.errorMsg = Object.keys(value.errors) | ||||
|         .map((key) => { | ||||
|           console.log(this.validationErrorMessages[key]); | ||||
|           return this.validationErrorMessages[key] || `Unknown error: ${key}`; | ||||
|         }) | ||||
|         .join(' '); | ||||
|   public showDeleteConfirmation = false; | ||||
| 
 | ||||
|   constructor(private http: HttpClient) { } | ||||
| 
 | ||||
|   public errorMessage: string = ''; | ||||
| 
 | ||||
|   ngOnInit() { | ||||
|     this.form = new FormGroup({ | ||||
|       name: new FormControl(this.hotel?.hotelName || '', [Validators.minLength(3), Validators.required]), | ||||
|       description: new FormControl(this.hotel?.description || '', [Validators.minLength(3), Validators.required]), | ||||
|       price: new FormControl(this.hotel?.price || '', [Validators.min(0), Validators.required]), | ||||
|       rating: new FormControl(this.hotel?.rating || '', [Validators.min(0), Validators.max(5), Validators.required]), | ||||
|       tags: new FormArray(this.hotel?.tags?.map(tag => new FormControl(tag)) || []) | ||||
|     }); | ||||
| 
 | ||||
|     Object.keys(this.form.controls).forEach(key => { | ||||
|       const control = this.form.get(key); | ||||
|       control?.valueChanges.pipe( | ||||
|         debounceTime(1000) | ||||
|       ).subscribe(() => {  | ||||
|         this.setError(control as AbstractControl, key); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   public delete() { | ||||
|     this.http.delete(`api/hotels/${this.hotel?.id}`).pipe( | ||||
|       catchError(err => { | ||||
|         console.error('Error deleting hotel', err); | ||||
|         return err; | ||||
|       }) | ||||
|     ).subscribe(() => { | ||||
|       console.log('Hotel deleted'); | ||||
|     }); | ||||
| 
 | ||||
|     this.router.navigate(['/']); | ||||
|   } | ||||
| 
 | ||||
|   get tags() { | ||||
|     return this.form.get('tags') as FormArray; | ||||
|   } | ||||
| 
 | ||||
|   public addTag() { | ||||
|     this.tags.push(new FormControl('')); | ||||
|   } | ||||
| 
 | ||||
|   public submit() { | ||||
|     if (this.form.valid && !this.errorMessage) { | ||||
|       const hotelData: Hotel = { | ||||
|         id: this.hotel?.id || 0, | ||||
|         hotelName: this.form.value.name, | ||||
|         description: this.form.value.description, | ||||
|         price: this.form.value.price, | ||||
|         rating: this.form.value.rating, | ||||
|         imageUrl: this.hotel?.imageUrl || 'https://placehold.co/2000x1050/EEE/31343C', | ||||
|         tags: this.form.value.tags || [] | ||||
|       }; | ||||
| 
 | ||||
|       if (this.hotel) { | ||||
|         this.http.put(`api/hotels/${this.hotel.id}`, hotelData).pipe( | ||||
|           catchError(err => { | ||||
|             console.error('Error updating hotel', err); | ||||
|             return err; | ||||
|           }) | ||||
|         ).subscribe(() => { | ||||
|           console.log('Hotel updated'); | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       else { | ||||
|         this.http.post('api/hotels', hotelData).pipe( | ||||
|           catchError(err => { | ||||
|             console.error('Error creating hotel', err); | ||||
|             return err; | ||||
|           }) | ||||
|         ).subscribe(() => { | ||||
|           console.log('Hotel created'); | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       this.form.reset(); | ||||
|       this.router.navigate(['/']); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   updateErrorMessages(): void { | ||||
|     this.errorMessages = {}; | ||||
|   private validationErrors: Record<string, string> = { | ||||
|     'required': 'This field is required', | ||||
|     'minlength': 'This field must be at least 3 characters long', | ||||
|     'min': 'This field must be at least 0', | ||||
|     'max': 'This field must be at most 5', | ||||
|     'name.required': 'Hotel name is required', | ||||
|     'name.minlength': 'Hotel name must be at least 3 characters long', | ||||
|     'description.required': 'Hotel description is required',  | ||||
|     'description.minlength': 'Hotel description must be at least 3 characters long', | ||||
|     'price.required': 'Hotel price is required', | ||||
|     'price.min': 'Hotel price cannot be negative', | ||||
|     'rating.required': 'Hotel rating is required', | ||||
|     'rating.min': 'Hotel rating must be at least 0', | ||||
|     'rating.max': 'Hotel rating cannot be more than 5', | ||||
|     'tags.required': 'At least one tag is required', | ||||
|   }; | ||||
| 
 | ||||
|     Object.keys(this.hotelForm.controls).forEach((field) => { | ||||
|       const control = this.hotelForm.get(field); | ||||
| 
 | ||||
|       if (control && control.errors) { | ||||
|         this.errorMessages[field] = Object.keys(control.errors) | ||||
|           .map( | ||||
|             (errorKey) => | ||||
|               this.validationErrorMessages[errorKey] || | ||||
|               `Unknown error: ${errorKey}`, | ||||
|           ) | ||||
|           .join(' '); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.hotelForm = new FormGroup({ | ||||
|       name: new FormControl(this.hotel.hotelName, Validators.required), | ||||
|       description: new FormControl(this.hotel.description, Validators.required), | ||||
|       price: new FormControl(this.hotel.price, Validators.required), | ||||
|     }); | ||||
| 
 | ||||
|     // this.hotelForm.valueChanges.subscribe(data => this.setMessage(this.hotelForm.get("name")!));
 | ||||
|     // this.hotelForm.valueChanges.subscribe(data => this.setMessage(this.hotelForm.get("description")!));
 | ||||
|     // this.hotelForm.valueChanges.subscribe(data => this.setMessage(this.hotelForm.get("price")!));
 | ||||
|     //
 | ||||
|     this.hotelForm.valueChanges.subscribe(() => this.updateErrorMessages()); | ||||
|   } | ||||
| 
 | ||||
|   submit(): void { | ||||
|     this.hotel.hotelName = this.hotelForm.get('name')?.value; | ||||
|     this.hotel.description = this.hotelForm.get('description')?.value; | ||||
|     this.hotel.price = this.hotelForm.get('price')?.value; | ||||
|     console.log(this.hotelForm.value); | ||||
|     this.http.put('/api/hotels/' + this.hotel.id, this.hotel).subscribe(); | ||||
|     this.router.navigate(['/']); | ||||
|   private setError(control: AbstractControl, controlName: string) { | ||||
|     if ((control.touched || control.dirty) && control.errors) { | ||||
|       const errors = Object.keys(control.errors).map(key => { | ||||
|         const errorKey = `${controlName}.${key}`; | ||||
|         return this.validationErrors[errorKey] || this.validationErrors[key]; | ||||
|       }); | ||||
|       this.errorMessage = errors.join(' '); | ||||
|     } else { | ||||
|       this.errorMessage = ''; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										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; | ||||
|   rating: number; | ||||
|   tags: Array<string>; | ||||
|   email?: string; | ||||
|   phone?: string; | ||||
| } | ||||
|  | @ -1,15 +1,28 @@ | |||
| <div class="container p-4 mx-auto max-w-4xl"> | ||||
|   <button (click)="loadTest2()">Load Test2</button> | ||||
|   <ng-container #test2></ng-container> | ||||
|   <h1 class="mt-6">{{ "hello" | uppercase | text }}</h1> | ||||
|   <a class="submit-button" routerLink="/new">CREATE NEW HOTEL</a> | ||||
|   <app-search [(input)]="search"></app-search> | ||||
|   @if (hotels[0].hotelName) { | ||||
|     <div *ngFor="let hotel of hotels"> | ||||
|       <app-hotel-item | ||||
|         *ngIf="hotel.hotelName.toLowerCase().includes(search.toLowerCase())" | ||||
|         [hotel]="hotel" | ||||
|       ></app-hotel-item> | ||||
|   <div class="fixed top-0 left-0 w-full bg-white/80 backdrop-blur-xl z-50"> | ||||
|     <div class="justify-center max-w-4xl container p-4 mx-auto"> | ||||
|       <h1 class="">{{'hello' | uppercase | text}}</h1> | ||||
|       <app-search [(input)]="search"></app-search> | ||||
|       <button routerLink="/create-hotel" mat-flat-button | ||||
|         class="btn btn-primary bg-blue-500 text-white font-semibold py-2 px-4 rounded-md shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75 my-4">Create | ||||
|         New Hotel</button> | ||||
|     </div> | ||||
|   } | ||||
|   </div> | ||||
|   <div *ngIf="hotels[0].hotelName" class="mt-64"> | ||||
|     <div *ngFor="let hotel of hotels"> | ||||
|       <app-hotel-item *ngIf="hotel.hotelName.toLowerCase().includes(search.toLowerCase())" [hotel]="hotel"></app-hotel-item> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <div *ngIf="!hotels[0].hotelName" class="mt-64"> | ||||
|     <div class="animate-pulse space-y-4"> | ||||
|       <div class="h-8 bg-gray-200 rounded w-52"></div> | ||||
|       <div class="space-y-3"> | ||||
|         <div class="h-4 bg-gray-200 rounded w-3/4"></div> | ||||
|         <div class="h-4 bg-gray-200 rounded w-11/12"></div> | ||||
|         <div class="h-4 bg-gray-200 rounded w-2/3"></div> | ||||
|       </div> | ||||
|       <div class="h-[300px] bg-gray-200 rounded w-full"></div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -8,8 +8,9 @@ describe('HotelListComponent', () => { | |||
| 
 | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       imports: [HotelListComponent], | ||||
|     }).compileComponents(); | ||||
|       imports: [HotelListComponent] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
| 
 | ||||
|     fixture = TestBed.createComponent(HotelListComponent); | ||||
|     component = fixture.componentInstance; | ||||
|  |  | |||
|  | @ -1,94 +1,33 @@ | |||
| import { CommonModule, NgFor, NgIf, UpperCasePipe } from '@angular/common'; | ||||
| import { Component, inject, ViewChild, ViewContainerRef } from '@angular/core'; | ||||
| import { NgFor, NgIf, UpperCasePipe } from '@angular/common'; | ||||
| import { Component, inject } from '@angular/core'; | ||||
| import { TextPipe } from '../../text.pipe'; | ||||
| import { SearchComponent } from '../Search/search.component'; | ||||
| import { HotelService } from '../Parent/services/hotel.service'; | ||||
| import { Hotel } from '../HotelItem/hotel'; | ||||
| import { SearchComponent } from '../search/search.component'; | ||||
| import { HotelService } from '../service/hotel.service'; | ||||
| import { Hotel } from '../hotel-item/hotel'; | ||||
| import { HttpClient } from '@angular/common/http'; | ||||
| import { filter, from, last, map, Observable, scan } from 'rxjs'; | ||||
| import { HotelItem } from '../HotelItem/HotelItem.component'; | ||||
| import { HotelItem } from '../hotel-item/HotelItem.component'; | ||||
| import { RouterLink } from '@angular/router'; | ||||
| 
 | ||||
| interface User { | ||||
|   name: string; | ||||
|   age: number; | ||||
| } | ||||
| import { MatButtonModule } from '@angular/material/button'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-hotel-list', | ||||
|   standalone: true, | ||||
|   imports: [ | ||||
|     UpperCasePipe, | ||||
|     TextPipe, | ||||
|     SearchComponent, | ||||
|     HotelItem, | ||||
|     NgFor, | ||||
|     NgIf, | ||||
|     RouterLink, | ||||
|   ], | ||||
|   imports: [UpperCasePipe, TextPipe, SearchComponent, HotelItem, NgFor, NgIf, RouterLink, MatButtonModule], | ||||
|   templateUrl: './hotel-list.component.html', | ||||
|   styleUrl: './hotel-list.component.css', | ||||
|   styleUrl: './hotel-list.component.css' | ||||
| }) | ||||
| export class HotelListComponent { | ||||
|   public search: string = ''; | ||||
|   public search: string = ""; | ||||
|   public hotelService: HotelService = inject(HotelService); | ||||
|   public response: any = null; | ||||
|   public hotels: Array<Hotel> = [{} as Hotel]; | ||||
| 
 | ||||
|   @ViewChild('test2', { read: ViewContainerRef }) | ||||
|   private test2ViewContainerRef!: ViewContainerRef; | ||||
| 
 | ||||
|   constructor(private http: HttpClient) { } | ||||
| 
 | ||||
|   async loadTest2() { | ||||
|     for (let i = 0; i < 1; i++) { | ||||
|       const { Test2Component } = await import('../test2/test2.component'); | ||||
| 
 | ||||
|       this.test2ViewContainerRef.createComponent(Test2Component); | ||||
|     } | ||||
|   constructor (private http: HttpClient) { | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit() { | ||||
|     this.http.get<Array<Hotel>>('api/hotels').subscribe((res) => { | ||||
|     this.http.get<Array<Hotel>>("api/hotels").subscribe(res => { | ||||
|       this.hotels = res; | ||||
|     }); | ||||
| 
 | ||||
|     const users = [ | ||||
|       { name: 'Max', age: 21 }, | ||||
|       { name: 'Peter', age: 31 }, | ||||
|       { name: 'Hans', age: 13 }, | ||||
|       { name: 'Klaus', age: 51 }, | ||||
|       { name: 'Dieter', age: 1 }, | ||||
|       { name: 'Jan-Marlon', age: 3 }, | ||||
|     ]; | ||||
| 
 | ||||
|     const stream: Observable<User> = from(users); | ||||
| 
 | ||||
|     stream | ||||
|       .pipe( | ||||
|         filter((user) => user.age > 18), | ||||
|         scan((acc, user) => acc + user.age, 0), | ||||
|         map((ageSum, index) => ageSum / (index + 1)), | ||||
|         last(), | ||||
|       ) | ||||
|       .subscribe(console.log); | ||||
| 
 | ||||
|     // const stream: Observable<number | string> = from([5, 1, 2, 12, 5, 14, 17, 5, "testing"]);
 | ||||
| 
 | ||||
|     // stream.pipe(
 | ||||
|     //  filter((value) => typeof value === "number"),
 | ||||
|     //  tap((value) => console.log("Zahl:" + value)),
 | ||||
|     //  filter((value: number) => value % 2 === 0),
 | ||||
|     //  tap((value) => console.log("Gerade Zahl: " + value)),
 | ||||
|     //  toArray(),
 | ||||
|     //).subscribe(console.log);
 | ||||
|   } | ||||
| 
 | ||||
|   public test() { | ||||
|     console.log(this.search); | ||||
|   } | ||||
| 
 | ||||
|   // public foundHotels = this.hotels.pipe(
 | ||||
|   //   filter((hotel) => hotel.hotelName.includes(this.search)),
 | ||||
|   // );
 | ||||
| } | ||||
|  |  | |||
|  | @ -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 { Test2Component } from './test2.component'; | ||||
| import { NotFoundComponent } from './not-found.component'; | ||||
| 
 | ||||
| describe('Test2Component', () => { | ||||
|   let component: Test2Component; | ||||
|   let fixture: ComponentFixture<Test2Component>; | ||||
| describe('NotFoundComponent', () => { | ||||
|   let component: NotFoundComponent; | ||||
|   let fixture: ComponentFixture<NotFoundComponent>; | ||||
| 
 | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       imports: [Test2Component] | ||||
|       imports: [NotFoundComponent] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
| 
 | ||||
|     fixture = TestBed.createComponent(Test2Component); | ||||
|     fixture = TestBed.createComponent(NotFoundComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     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 () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       imports: [StarRatingComponent], | ||||
|     }).compileComponents(); | ||||
|       imports: [StarRatingComponent] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
| 
 | ||||
|     fixture = TestBed.createComponent(StarRatingComponent); | ||||
|     component = fixture.componentInstance; | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ import { Component, Input } from '@angular/core'; | |||
|   standalone: true, | ||||
|   imports: [NgClass, NgFor], | ||||
|   templateUrl: './star-rating.component.html', | ||||
|   styleUrl: './star-rating.component.css', | ||||
|   styleUrl: './star-rating.component.css' | ||||
| }) | ||||
| export class StarRatingComponent { | ||||
|   @Input() public rating: number = 0; | ||||
|  |  | |||
|  | @ -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({ | ||||
|   name: 'jkcurrency', | ||||
|   standalone: true, | ||||
|   standalone: true | ||||
| }) | ||||
| export class CurrencyPipe implements PipeTransform { | ||||
|   transform(value: string): string { | ||||
|     return value.replaceAll('L', 'P'); | ||||
|     return value.replaceAll("L", "P"); | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,14 +1,16 @@ | |||
| <!doctype html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="utf-8" /> | ||||
|     <title>HotelManager</title> | ||||
|     <base href="/" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||||
|     <link rel="icon" type="image/x-icon" href="favicon.ico" /> | ||||
|     <link rel="stylesheet" href="./styles.css" /> | ||||
|   </head> | ||||
|   <body class="bg-pink-600 p-32"> | ||||
|     <app-root></app-root> | ||||
|   </body> | ||||
| <head> | ||||
|   <meta charset="utf-8"> | ||||
|   <title>HotelManager</title> | ||||
|   <base href="/"> | ||||
|   <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|   <link rel="icon" type="image/x-icon" href="favicon.ico"> | ||||
|   <link rel="stylesheet" href="./styles.css"> | ||||
|   <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> | ||||
|   <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> | ||||
| </head> | ||||
| <body class="mat-typography"> | ||||
|   <app-root></app-root> | ||||
| </body> | ||||
| </html> | ||||
|  |  | |||
|  | @ -2,6 +2,5 @@ import { bootstrapApplication } from '@angular/platform-browser'; | |||
| import { appConfig } from './app/app.config'; | ||||
| import { AppComponent } from './app/app.component'; | ||||
| 
 | ||||
| bootstrapApplication(AppComponent, appConfig).catch((err) => | ||||
|   console.error(err), | ||||
| ); | ||||
| bootstrapApplication(AppComponent, appConfig) | ||||
|   .catch((err) => console.error(err)); | ||||
|  |  | |||
|  | @ -7,18 +7,5 @@ | |||
|   @apply border-black rounded px-3 py-1 border-[3px]; | ||||
| } | ||||
| 
 | ||||
| .input-field { | ||||
|   @apply border-red-500 text-3xl rounded-full bg-gray-500 font-bold p-3 m-3 border-8; | ||||
| } | ||||
| 
 | ||||
| .back-button { | ||||
|   @apply border-black rounded bg-blue-500 border-[30px] m-3 text-3xl font-bold p-3; | ||||
| } | ||||
| 
 | ||||
| .submit-button { | ||||
|   @apply border-black rounded-full bg-green-500 border-[30px] m-3 text-3xl font-bold p-3; | ||||
| } | ||||
| 
 | ||||
| .delete-button { | ||||
|   @apply border-black rounded bg-red-500 border-[30px] m-3 text-3xl font-bold p-3; | ||||
| } | ||||
| html, body { height: 100%; } | ||||
| body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } | ||||
|  | @ -1,11 +1,11 @@ | |||
| import { Pipe, PipeTransform } from '@angular/core'; | ||||
| import {Pipe, PipeTransform} from '@angular/core'; | ||||
| 
 | ||||
| @Pipe({ | ||||
|   name: 'text', | ||||
|   standalone: true, | ||||
|   standalone: true | ||||
| }) | ||||
| export class TextPipe implements PipeTransform { | ||||
|   transform(value: string): string { | ||||
|     return value.replaceAll('L', 'P'); | ||||
|     return value.replaceAll("L", "P"); | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,25 +1,9 @@ | |||
| /** @type {import('tailwindcss').Config} */ | ||||
| 
 | ||||
| const colors = require("tailwindcss/colors"); | ||||
| 
 | ||||
| module.exports = { | ||||
|   content: ["./src/**/*.{html,ts}"], | ||||
|   theme: { | ||||
|     extend: { | ||||
|       colors: { | ||||
|         ...colors, | ||||
|       }, | ||||
|       keyframes: { | ||||
|         shake: { | ||||
|           "0%, 100%": { transform: "translateX(0)" }, | ||||
|           "25%": { transform: "translateX(-30px)" }, | ||||
|           "75%": { transform: "translateX(30px)" }, | ||||
|         }, | ||||
|       }, | ||||
|       animation: { | ||||
|         shake: "shake 10s ease-in-out infinite", | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   content: [ | ||||
|     "./src/**/*.{html,ts}", | ||||
|   ], | ||||
|   plugins: [], | ||||
| }; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,6 +6,10 @@ | |||
|     "outDir": "./out-tsc/app", | ||||
|     "types": [] | ||||
|   }, | ||||
|   "files": ["src/main.ts"], | ||||
|   "include": ["src/**/*.d.ts"] | ||||
|   "files": [ | ||||
|     "src/main.ts" | ||||
|   ], | ||||
|   "include": [ | ||||
|     "src/**/*.d.ts" | ||||
|   ] | ||||
| } | ||||
|  |  | |||
|  | @ -18,7 +18,10 @@ | |||
|     "importHelpers": true, | ||||
|     "target": "ES2022", | ||||
|     "module": "ES2022", | ||||
|     "lib": ["ES2022", "dom"] | ||||
|     "lib": [ | ||||
|       "ES2022", | ||||
|       "dom" | ||||
|     ] | ||||
|   }, | ||||
|   "angularCompilerOptions": { | ||||
|     "enableI18nLegacyMessageIdFormat": false, | ||||
|  |  | |||
|  | @ -4,7 +4,12 @@ | |||
|   "extends": "./tsconfig.json", | ||||
|   "compilerOptions": { | ||||
|     "outDir": "./out-tsc/spec", | ||||
|     "types": ["jasmine"] | ||||
|     "types": [ | ||||
|       "jasmine" | ||||
|     ] | ||||
|   }, | ||||
|   "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] | ||||
|   "include": [ | ||||
|     "src/**/*.spec.ts", | ||||
|     "src/**/*.d.ts" | ||||
|   ] | ||||
| } | ||||
|  |  | |||
		Reference in a new issue