Compare commits

..

No commits in common. "main" and "constantin" have entirely different histories.

88 changed files with 2996 additions and 5566 deletions

View File

@ -1 +0,0 @@
flake-profile-4-link

View File

@ -1 +0,0 @@
/nix/store/3hvzb7brklcrpdggn78fnmi20i49jnys-nix-shell-env

View File

@ -1 +0,0 @@
/nix/store/amb6zvwfmy0l7yfdhrfaclcy4997fy03-nix-shell-env

View File

@ -1 +0,0 @@
/nix/store/rsadgr5y0wa25gvk7j7zcml5pf9ym3h5-nix-shell-env

View File

@ -1 +0,0 @@
/nix/store/amb6zvwfmy0l7yfdhrfaclcy4997fy03-nix-shell-env

1
.envrc
View File

@ -1 +0,0 @@
use flake

1
.gitignore vendored
View File

@ -40,3 +40,4 @@ testem.log
# System files
.DS_Store
Thumbs.db
.angular

View File

@ -1,4 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

20
.vscode/launch.json vendored
View File

@ -1,20 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

42
.vscode/tasks.json vendored
View File

@ -1,42 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

View File

@ -24,8 +24,7 @@
{
"glob": "**/*",
"input": "public"
},
"src/assets"
}
],
"styles": [
"src/styles.css"
@ -93,5 +92,8 @@
}
}
}
},
"cli": {
"analytics": false
}
}

BIN
bun.lockb

Binary file not shown.

View File

@ -1,23 +0,0 @@
{
name = "hotel-manager";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
};
packages = [
pkgs.nodePackages."@angular/cli"
pkgs.git
pkgs.bun
];
services.ng-serve = {
description = "Angular Development Server";
start = ''
echo "Starting Angular server..."
ng serve --host 0.0.0.0 --port 4200
'';
restartIfChanged = true;
after = [ "network-online.target" ];
};
}

27
flake.lock generated
View File

@ -1,27 +0,0 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1725634671,
"narHash": "sha256-v3rIhsJBOMLR8e/RNWxr828tB+WywYIoajrZKFM+0Gg=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "574d1eac1c200690e27b8eb4e24887f8df7ac27c",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View File

@ -1,31 +0,0 @@
{
description = "Hotel-Manager";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
};
outputs = { self, nixpkgs, ... }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.${system}.default =
pkgs.mkShell
{
buildInputs = [
pkgs.nodePackages."@angular/cli"
pkgs.git
pkgs.bun
];
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
shellHook = ''
export CARGO_HOME=$HOME/.cargo
export RUSTUP_HOME=$HOME/.rustup
'';
};
};
}

6736
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,23 +10,23 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^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/common": "^18.2.3",
"@angular/compiler": "^18.2.3",
"@angular/core": "^18.2.3",
"@angular/forms": "^18.2.3",
"@angular/platform-browser": "^18.2.3",
"@angular/platform-browser-dynamic": "^18.2.3",
"@angular/router": "^18.2.3",
"angular-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",
@ -35,8 +35,8 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"postcss": "^8.4.41",
"tailwindcss": "^3.4.10",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.15",
"typescript": "~5.5.2"
}
}

View File

Before

Width:  |  Height:  |  Size: 637 KiB

After

Width:  |  Height:  |  Size: 637 KiB

View File

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 142 KiB

View File

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 143 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
public/assets/img/huy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

BIN
public/assets/img/kjan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
public/assets/img/rat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

View File

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

View File

@ -1 +0,0 @@
sonar.projectKey=Hotel-Manager

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component, NgModule } from "@angular/core";
import { FormsModule, NgForm, NgModel } from "@angular/forms";
import { Input, Output } from "@angular/core";
import { EventEmitter } from "@angular/core";
@Component({
selector: 'app-search',
standalone: true,
templateUrl: './search.component.html',
imports: [FormsModule],
})
export class SearchComponent {
@Input() public input: string = "";
@Output() inputChange = new EventEmitter<string>();
public update(e: string) {
this.inputChange.emit(e);
}
}

View File

@ -1,58 +0,0 @@
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { Hotel } from '../HotelItem/hotel';
/**
* Initial data for in memory web api
*
* @export
* @class HotelData
* @implements {InMemoryDbService}
*/
export class HotelData implements InMemoryDbService {
createDb(): Record<string, Hotel[]> {
const hotels: Hotel[] = [
{
id: 1,
hotelName: 'Buea sweet life',
description: 'Belle vue au bord de la mer',
price: 230.5,
imageUrl: 'assets/img/1.jpg',
rating: 3.5,
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']
}, {
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']
}, {
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']
}
];
return { hotels };
}
genId(hotels: Hotel[]): number {
return hotels.length > 0 ? Math.max(...hotels.map(hotel => hotel.id)) + 1 : 1;
}
}

View File

@ -1,3 +1 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

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

View File

@ -1,24 +1,70 @@
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 { RouterOutlet } from '@angular/router';
import {Component, OnInit} from '@angular/core';
import {HotelsComponent} from "./hotel/hotels.component";
import {IdkComponent} from "./idek/idk.component";
import {filter, from, map, reduce} from "rxjs";
import {RouterOutlet} from "@angular/router";
@Injectable({providedIn: "root"})
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, NgFor, NgForOf, NgIf, HotelItem, SearchComponent, UpperCasePipe, TextPipe, AsyncPipe],
templateUrl: './app.component.html',
providers: [HotelService],
styleUrl: './app.component.css'
imports: [HotelsComponent, IdkComponent, RouterOutlet],
template: `
<router-outlet />
`
})
export class AppComponent {
}
export class AppComponent implements OnInit {
ngOnInit() {
const users = [
{
name: 'John',
age: 15,
},
{
name: 'Alice',
age: 16,
},
{
name: 'Bob',
age: 45,
},
{
name: 'Eve',
age: 29,
},
{
name: 'Charlie',
age: 52,
},
{
name: 'Dave',
age: 41,
},
{
name: 'Mallory',
age: 37,
},
{
name: 'Trent',
age: 48,
},
{
name: 'Peggy',
age: 26,
},
{
name: 'Victor',
age: 39,
}
];
from(users).pipe(
filter(user => user.age >= 18),
reduce((acc, user) => {
acc.age += user.age
acc.count++;
return acc;
}, {age: 0, count: 0}),
map(data => data.age / data.count),
).subscribe((data) => {console.log("avg age: ", data)});
}
}

View File

@ -1,20 +1,22 @@
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';
import { routes } from './app.routes';
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 {registerLocaleData} from "@angular/common";
import localeDe from "@angular/common/locales/de"
import localeCn from "@angular/common/locales/en"
import localeJap from "@angular/common/locales/en"
import {provideHttpClient} from "@angular/common/http";
import {InMemoryWebApiModule} from "angular-in-memory-web-api";
import {HotelDataService} from "./service/HotelData.service";
registerLocaleData(localeDe, 'de-DE');
registerLocaleData(localeCn, 'cn-CN');
registerLocaleData(localeDe, 'de-DE')
registerLocaleData(localeCn, 'cn-CN')
registerLocaleData(localeJap, 'ja-JP')
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(),
importProvidersFrom(InMemoryWebApiModule.forRoot(HotelData))]
importProvidersFrom(InMemoryWebApiModule.forRoot(HotelDataService))
]
};

View File

@ -1,24 +1,22 @@
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 {Routes} from '@angular/router';
import {HotelsComponent} from "./hotel/hotels.component";
import {HotelComponent} from "./hotel/hotel.component";
import {CreateHotelComponent} from "./hotel/create-hotel.component";
export const routes: Routes = [
{
path: "",
component: HotelListComponent,
path: 'hotels',
component: HotelsComponent,
title: 'Hotels',
},
{
path: "hotels/:id",
component: HotelDetailsComponent,
path: 'hotels/new',
component: CreateHotelComponent,
title: 'New Hotel'
},
{
path: "testing",
component: TestComponent,
},
{
path: "new",
component: NewHotelComponent,
path: 'hotels/:hotelId',
component: HotelComponent,
title: 'Hotel',
}
];

View File

@ -1,34 +0,0 @@
import { AbstractControl, FormGroup, ValidationErrors, ValidatorFn } from "@angular/forms";
export class CrossValidator {
public onlyAllowNameAndDescriptionSame() {
return (control: AbstractControl): ValidationErrors | null => {
if (!(control instanceof FormGroup)) {
return null;
}
const name = control.get('name')?.value;
const description = control.get('description')?.value;
console.log(name, description);
const error = name !== description ? { mismatch: true } : null;
console.log(error);
return error;
};
}
public crossValidate() {
return (control: AbstractControl): ValidationErrors | null => {
if (!(control instanceof FormGroup)) {
return null;
}
const contactType = control.get('contactType')?.value;
if (contactType == "None") return null;
}
}
}

View File

@ -0,0 +1,57 @@
import {Component, EventEmitter, Output} from "@angular/core";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {Lang} from "../idek/lang";
@Component({
standalone: true,
selector: "app-currency",
imports: [
ReactiveFormsModule,
FormsModule
],
template: `
<select [ngModel]="currency" (ngModelChange)="setCurrency($event)">
@for (currency of currencies; track currency.code) {
<option value="{{currency.name}}">{{ currency.name }}</option>
}
</select>
`
})
export class CurrencyComponent {
@Output()
public currency: EventEmitter<Lang> = new EventEmitter();
public currencies: Lang[] = [
{
name: 'de',
code: 'de-DE',
currency: 'EUR'
},
{
name: 'en',
code: 'en-US',
currency: 'USD'
},
{
name: 'jap',
code: 'ja-JP',
currency: 'JPY'
},
{
name: 'cn',
code: 'cn-CN',
currency: 'CNY'
}
]
public setCurrency(currencyInput: string): void {
for (const currency of this.currencies) {
if (currency.name === currencyInput) {
this.currency.emit(currency);
break;
}
}
}
}

View File

@ -1,5 +0,0 @@
@if (hotel) {
<app-hotel-item [hotel]="hotel" [isDetail]="true"></app-hotel-item>
<app-hotel-form [hotel]="hotel"></app-hotel-form>
<button class="delete-button" (click)="delete()">😠 DELETE</button>
}

View File

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

View File

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

View File

@ -1,18 +0,0 @@
<p>hotel-form works!</p>
<h1>{{errorMsg}}</h1>
<form [formGroup]="hotelForm">
<label for="name">Name</label>
<div class="text-red-500 text-5xl font-bold" *ngIf="errorMessages['name']">{{ errorMessages['name'] }}</div>
<input type="text" class="border-red-500 text-3xl border-3 rounded-full bg-gray-500 font-bold p-3 m-3 border-8" id="name" formControlName="name">
<label for="description">Description</label>
<div class="text-red" *ngIf="errorMessages['description']">{{ errorMessages['description'] }}</div>
<input type="text" class="input-field" [class.border-8]='hotelForm.get("description")?.invalid' id="description"
formControlName="description">
<label for="price">Price</label>
<div class="text-red" *ngIf="errorMessages['price']">{{ errorMessages['price'] }}</div>
<input type="number" class="input-field"
[class.border-8]='hotelForm.get("price")?.invalid && hotelForm.get("price")?.touched' id="price"
formControlName="price">
<button class="submit-button" (click)="submit()" [disabled]="!hotelForm.valid" type="submit">😃 Submit</button>
<a class="back-button" routerLink="/">😭 Cancel</a>
</form>

View File

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

View File

@ -1,77 +0,0 @@
import { Component, Input } from '@angular/core';
import { Hotel } from '../HotelItem/hotel';
import { AbstractControl, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { NgFor, NgIf } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Router, RouterLink } from '@angular/router';
@Component({
selector: 'app-hotel-form',
standalone: true,
imports: [ReactiveFormsModule, NgIf, NgFor, RouterLink],
templateUrl: './hotel-form.component.html',
styleUrl: './hotel-form.component.css'
})
export class HotelFormComponent {
@Input() public hotel!: Hotel;
public hotelForm!: FormGroup
public errorMsg: string = 'testing';
errorMessages: Record<string, string> = {};
constructor(public http: HttpClient, public router: Router) {
}
private validationErrorMessages: Record<string, string> = {
required: "This field is required",
minlength: "The value is too short",
maxlength: "The value is too long",
pattern: "The value format is incorrect"
};
setMessage(value: AbstractControl): void {
if ((value.touched || value.dirty) && value.errors) {
this.errorMsg = Object.keys(value.errors).map(key => {
console.log(this.validationErrorMessages[key]);
return this.validationErrorMessages[key] || `Unknown error: ${key}`;
}).join(' ');
}
}
updateErrorMessages(): void {
this.errorMessages = {};
Object.keys(this.hotelForm.controls).forEach(field => {
const control = this.hotelForm.get(field);
if (control && control.errors) {
this.errorMessages[field] = Object.keys(control.errors)
.map(errorKey => this.validationErrorMessages[errorKey] || `Unknown error: ${errorKey}`)
.join(' ');
}
});
}
ngOnInit(): void {
this.hotelForm = new FormGroup({
name: new FormControl(this.hotel.hotelName, Validators.required),
description: new FormControl(this.hotel.description, Validators.required),
price: new FormControl(this.hotel.price, Validators.required),
});
// this.hotelForm.valueChanges.subscribe(data => this.setMessage(this.hotelForm.get("name")!));
// this.hotelForm.valueChanges.subscribe(data => this.setMessage(this.hotelForm.get("description")!));
// this.hotelForm.valueChanges.subscribe(data => this.setMessage(this.hotelForm.get("price")!));
//
this.hotelForm.valueChanges.subscribe(() => this.updateErrorMessages());
}
submit(): void {
this.hotel.hotelName = this.hotelForm.get("name")?.value;
this.hotel.description = this.hotelForm.get("description")?.value;
this.hotel.price = this.hotelForm.get("price")?.value;
console.log(this.hotelForm.value);
this.http.put("/api/hotels/" + this.hotel.id, this.hotel).subscribe();
this.router.navigate(["/"]);
}
}

View File

@ -1,10 +0,0 @@
<div class="container p-4 mx-auto max-w-4xl">
<h1>{{'hello' | uppercase | text}}</h1>
<a class="submit-button" routerLink="/new">CREATE NEW HOTEL</a>
<app-search [(input)]="search"></app-search>
@if (hotels[0].hotelName) {
<div *ngFor="let hotel of hotels">
<app-hotel-item *ngIf="hotel.hotelName.toLowerCase().includes(search.toLowerCase())" [hotel]="hotel"></app-hotel-item>
</div>
}
</div>

View File

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

View File

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

View File

@ -0,0 +1,29 @@
import {Component, inject} from "@angular/core";
import {Hotel} from "./hotel";
import {HotelService} from "../service/hotel.service";
import {EditHotelComponent} from "./edit-hotel.component";
import {Router} from "@angular/router";
@Component({
selector: "app-create-hotel",
standalone: true,
imports: [
EditHotelComponent
],
template: `
<app-edit-hotel (updateHotel)="create($event)"></app-edit-hotel>
`
})
export class CreateHotelComponent {
hotelService: HotelService = inject(HotelService);
router: Router = inject(Router);
create(hotel: Hotel) {
this.hotelService.createHotel(hotel).subscribe();
this.router.navigate(['/hotels']);
}
}

View File

@ -0,0 +1,27 @@
import {Component, inject, Input} from "@angular/core";
import {HotelService} from "../service/hotel.service";
import {Router} from "@angular/router";
@Component({
selector: 'app-delete-hotel',
standalone: true,
template: `
<button (click)="delete()">Delete Hotel</button>
`
})
export class DeleteHotelComponent {
@Input()
id: number = 0;
hotelService: HotelService = inject(HotelService);
router: Router = inject(Router);
delete() {
if (confirm('Are you sure?')) {
this.hotelService.deleteById(this.id).subscribe()
this.router.navigate(['/hotels']);
}
}
}

View File

@ -0,0 +1,118 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {
AbstractControl,
FormArray,
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule,
Validators
} from "@angular/forms";
import {Hotel} from "./hotel";
import {RouterLink} from "@angular/router";
import {NgForOf} from "@angular/common";
import {TagComponent} from "./tag.component";
@Component({
selector: 'app-edit-hotel',
standalone: true,
imports: [
FormsModule,
ReactiveFormsModule,
RouterLink,
NgForOf,
TagComponent
],
template: `
<ng-form [formGroup]="form" (ngSubmit)="submit()">
<label for="name">Name</label>
<br>
<input id="name" type="text" formControlName="name">
<br>
<label for="description">Description</label>
<br>
<input id="description" type="text" formControlName="description">
<br>
<label for="price">Price</label>
<br>
<input id="price" type="number" formControlName="price">
<br>
<label for="rating">Rating</label>
<br>
<input id="rating" type="number" formControlName="rating" min="0" max="5">
<br>
<br>
<button (click)="addTag()">+</button>
<div formArrayName="tags">
<div *ngFor="let tag of getTags().controls; let i = index">
<input type="text" placeholder="tag" [formControlName]="i">
<button (click)="deleteTag(tag)">-</button>
</div>
</div>
<br>
<br>
<button type="submit" (click)="submit()">Submit</button>
</ng-form>
<button routerLink="/hotels">back</button>
`,
})
export class EditHotelComponent implements OnInit {
@Input()
hotel: Hotel | null = null;
@Output()
updateHotel: EventEmitter<Hotel> = new EventEmitter
form!: FormGroup
ngOnInit(): void {
const tags = [];
for (const tag of this.hotel?.tags ?? []) {
tags.push(new FormControl(tag, [Validators.required]));
}
console.log(tags)
this.form = 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]),
rating: new FormControl(this.hotel?.rating, [Validators.required, Validators.min(0), Validators.max(5)]),
tags: new FormArray(tags),
})
}
submit() {
if (!this.form.valid) {
console.error('Form invalid');
}
const hotel: Hotel = {
imageUrl: this.hotel?.imageUrl ?? "",
hotelName: this.form.value.name,
description: this.form.value.description,
price: this.form.value.price,
rating: this.form.value.rating,
id: this.hotel?.id ?? 0,
tags: this.form.value.tags ?? []
};
console.log(hotel)
this.updateHotel.emit(hotel)
this.hotel = hotel;
}
getTags() {
return this.form.controls['tags'] as FormArray;
}
addTag() {
this.getTags().push(new FormControl('', [Validators.required]));
}
deleteTag(tagElement: AbstractControl) {
this.getTags().removeAt(this.getTags().controls.indexOf(tagElement));
}
}

View File

@ -0,0 +1,61 @@
import {Component, inject, OnInit} from "@angular/core";
import {Hotel} from "./hotel"
import {CurrencyPipe, NgOptimizedImage} from "@angular/common";
import {Lang} from "../idek/lang";
import {StarComponent} from "../star/star.component";
import {ActivatedRoute} from "@angular/router";
import {HotelService} from "../service/hotel.service";
import {catchError, EMPTY} from "rxjs";
import {EditHotelComponent} from "./edit-hotel.component";
import {DeleteHotelComponent} from "./delete-hotel.component";
@Component({
standalone: true,
selector: 'app-hotel',
imports: [
CurrencyPipe,
StarComponent,
NgOptimizedImage,
EditHotelComponent,
DeleteHotelComponent
],
template: `
<div style="border: white 2px; border-radius: 2px">
@if (hotel && !alert) {
<app-edit-hotel [hotel]="hotel" (updateHotel)="update($event)" />
<app-delete-hotel [id]="hotel.id" />
} @else if(alert) {
<h2>{{alert}}</h2>
} @else {
<h2>loading</h2>
}
</div>
`
})
export class HotelComponent implements OnInit {
protected currency: Lang = {name: 'de', code: 'de-DE', currency: 'EUR'}
protected hotel!: Hotel;
protected alert: string|undefined
private route: ActivatedRoute = inject(ActivatedRoute);
private hotelService: HotelService = inject(HotelService);
ngOnInit(): void {
const hotelId = this.route.snapshot.params['hotelId'];
this.hotelService.getHotelById(hotelId)
.pipe(catchError((err) => {
this.alert = `Hotel could not be retrieved: ${err.message}`
return EMPTY;
}))
.subscribe({next: (hotel: Hotel) => {this.hotel = hotel}})
}
update(hotel: Hotel): void {
this.hotelService.updateHotelById(hotel).subscribe()
}
}

View File

@ -5,5 +5,5 @@ export interface Hotel {
price: number;
imageUrl: string;
rating: number;
tags: Array<string>;
tags: string[];
}

View File

@ -0,0 +1,49 @@
import {Component, inject} from "@angular/core";
import {HotelComponent} from "./hotel.component";
import {Hotel} from "./hotel";
import {FormsModule} from "@angular/forms";
import {Lang} from "../idek/lang";
import {HotelService} from "../service/hotel.service";
import {filter, Observable, toArray} from "rxjs";
import {AsyncPipe} from "@angular/common";
import {CurrencyComponent} from "../currency/currency.component";
import {RouterLink} from "@angular/router";
import {StarComponent} from "../star/star.component";
@Component({
standalone: true,
template: `
<app-currency (currency)="currency = $event"></app-currency>
<form>
<input name="search" [ngModel]="search" (ngModelChange)="searchEvent($event)">
</form>
<p routerLink="/hotels/new">Create Hotel</p>
@for (hotel of (matchingHotels | async); track hotel.id) {
<div>{{hotel.hotelName}}</div>
<img src="{{hotel.imageUrl}}" alt="{{hotel.hotelName}}" height="64" width="64">
<br>
<button routerLink="{{hotel.id}}">Details</button>
<hr>
} @empty {
<h1>no matching results for {{ search }}</h1>
}
`,
imports: [FormsModule, HotelComponent, AsyncPipe, CurrencyComponent, RouterLink, StarComponent],
providers: [HotelService],
selector: 'app-hotels'
})
export class HotelsComponent {
public currency: Lang = {name: 'de', code: 'de-DE', currency: 'EUR'};
public search: string = '';
private hotelService: HotelService= inject(HotelService);
public matchingHotels: Observable<Hotel[]> = this.hotelService.getHotels();
public searchEvent(input: string) {
this.search = input.toLowerCase();
}
}

View File

@ -0,0 +1,33 @@
import {Component, EventEmitter, Input, Output} from "@angular/core";
import {AbstractControl, ControlContainer, FormGroupDirective, ReactiveFormsModule} from "@angular/forms";
import {JsonPipe} from "@angular/common";
@Component({
standalone: true,
selector: 'app-tag',
imports: [
ReactiveFormsModule,
JsonPipe
],
viewProviders: [
{
provide: ControlContainer,
useExisting: FormGroupDirective
}
],
template: `
`
})
export class TagComponent {
@Input()
tagElement!: AbstractControl;
@Output()
deleteEvent: EventEmitter<AbstractControl> = new EventEmitter();
delete() {
this.deleteEvent.emit(this.tagElement);
}
}

View File

@ -0,0 +1,53 @@
import {Component} from "@angular/core";
import {CurrencyPipe} from "@angular/common";
import {FormsModule} from "@angular/forms";
import {Lang} from "./lang";
@Component({
selector: 'app-idk',
standalone: true,
imports: [CurrencyPipe, FormsModule],
template: `
<select [ngModel]="currency" (ngModelChange)="setCurrency($event)">
@for (currency of currencies; track null) {
<option value="{{currency.name}}">{{currency.name}}</option>
}
</select>
{{ 234 | currency : currency.currency : 'symbol' : '2.2-2' : currency.code }}
`
})
export class IdkComponent {
public currency: Lang = {name: 'de', code: 'de-DE', currency: 'EUR'};
public currencies: Lang[] = [
{
name: 'de',
code: 'de-DE',
currency: 'EUR'
},
{
name: 'en',
code: 'en-US',
currency: 'USD'
},
{
name: 'jap',
code: 'ja-JP',
currency: 'JPY'
},
{
name: 'cn',
code: 'cn-CN',
currency: 'CNY'
}
]
public setCurrency(currencyInput: string): void {
for (const currency of this.currencies) {
if (currency.name === currencyInput) {
this.currency = currency;
break;
}
}
}
}

5
src/app/idek/lang.ts Normal file
View File

@ -0,0 +1,5 @@
export interface Lang {
code: string;
name: string;
currency: string;
}

11
src/app/idek/test.pipe.ts Normal file
View File

@ -0,0 +1,11 @@
import {Pipe, PipeTransform} from "@angular/core";
@Pipe({
name: 'idk',
standalone: true,
})
export class TestPipe implements PipeTransform {
transform(value: string): string {
return value.toLowerCase().replaceAll('l', 'p');
}
}

View File

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

View File

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

View File

@ -1,117 +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",
}
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().onlyAllowNameAndDescriptionSame() })
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
}
this.http.post("/api/hotels/", hotel).subscribe();
this.router.navigate(["/"]);
}
}

View File

@ -1,11 +1,10 @@
import { Injectable } from "@angular/core";
import { Hotel } from "../../HotelItem/hotel";
import { from, Observable } from "rxjs";
import {InMemoryDbService} from "angular-in-memory-web-api";
import {Hotel} from "../hotel/hotel";
@Injectable()
export class HotelService {
public getHotels(): Observable<Hotel> {
return from([
export class HotelDataService implements InMemoryDbService{
createDb(): Record<string, Hotel[]> {
const hotels: Hotel[] = [
{
"id": 1,
"hotelName": "Buea süßes Leben",
@ -13,7 +12,7 @@ export class HotelService {
"price": 230.5,
"imageUrl": "assets/img/1.jpg",
"rating": 3.5,
"tags": ["test"]
"tags": ["Meer", "Berge"]
},
{
"id": 2,
@ -22,7 +21,7 @@ export class HotelService {
"price": 145.5,
"imageUrl": "assets/img/2.jpg",
"rating": 5,
"tags": ["test"]
"tags": ["Meer", "Berge"]
},
{
"id": 3,
@ -31,17 +30,20 @@ export class HotelService {
"price": 120.12,
"imageUrl": "assets/img/3.jpg",
"rating": 4,
"tags": ["test"]
"tags": ["Meer", "Berge"]
},
{
"id": 4,
"hotelName": "Kapstadt Stadt",
"hotelName": "OUR Hotel",
"description": "Wunderschönes Ambiente für Ihren Aufenthalt",
"price": 135.12,
"imageUrl": "assets/img/4.jpg",
"rating": 2.5,
"tags": ["test"]
"tags": ["Meer", "Berge"]
}
]);
];
return { hotels };
}
}

View File

@ -0,0 +1,31 @@
import {HttpClient} from "@angular/common/http";
import {inject, Injectable} from "@angular/core";
import {Observable} from "rxjs";
import {Hotel} from "../hotel/hotel";
@Injectable({
providedIn: "root",
})
export class HotelService {
private httpClient: HttpClient = inject(HttpClient);
public getHotels(): Observable<Hotel[]> {
return this.httpClient.get<Hotel[]>('/api/hotels');
}
public getHotelById(id: number): Observable<Hotel> {
return this.httpClient.get<Hotel>(`/api/hotels/${id}`);
}
public updateHotelById(hotel: Hotel): Observable<Object> {
return this.httpClient.put<Hotel>(`/api/hotels/${hotel.id}`, hotel)
}
public createHotel(hotel: Hotel): Observable<Hotel> {
return this.httpClient.post<Hotel>(`/api/hotels`, hotel)
}
public deleteById(id: number): Observable<Hotel> {
return this.httpClient.delete<Hotel>(`/api/hotels/${id}`);
}
}

View File

@ -1,35 +0,0 @@
<div class="flex items-center">
<div *ngFor="let star of getRatingArray()" class="flex flex-inline">
<svg
[ngClass]="{ hidden: !(star == 0.5) }"
class="w-4 h-4 text-yellow-300 me-1"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 22 20"
>
<path
d="M13 4.024v-.005c0-.053.002-.353-.217-.632a1.01 1.01 0 0 0-1.176-.315c-.192.076-.315.193-.35.225c-.052.05-.094.1-.122.134a4 4 0 0 0-.31.457c-.207.343-.484.84-.773 1.375a169 169 0 0 0-1.606 3.074h-.002l-4.599.367c-1.775.14-2.495 2.339-1.143 3.488L6.17 15.14l-1.06 4.406c-.412 1.72 1.472 3.078 2.992 2.157l3.94-2.388c.592-.359.958-.996.958-1.692v-13.6Zm-2.002 0v.025z"
/>
</svg>
<svg
[ngClass]="{ hidden: !(star == 1) }"
class="w-4 h-4 text-yellow-300 me-1"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 22 20"
>
<path
d="M20.924 7.625a1.523 1.523 0 0 0-1.238-1.044l-5.051-.734-2.259-4.577a1.534 1.534 0 0 0-2.752 0L7.365 5.847l-5.051.734A1.535 1.535 0 0 0 1.463 9.2l3.656 3.563-.863 5.031a1.532 1.532 0 0 0 2.226 1.616L11 17.033l4.518 2.375a1.534 1.534 0 0 0 2.226-1.617l-.863-5.03L20.537 9.2a1.523 1.523 0 0 0 .387-1.575Z"
/>
</svg>
</div>
<p class="ms-1 text-sm font-medium text-gray-500 dark:text-gray-400">
{{ rating }}
</p>
<p class="ms-1 text-sm font-medium text-gray-500 dark:text-gray-400">
out of
</p>
<p class="ms-1 text-sm font-medium text-gray-500 dark:text-gray-400">5</p>
</div>

View File

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

View File

@ -1,29 +0,0 @@
import { NgClass, NgFor } from '@angular/common';
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-star-rating',
standalone: true,
imports: [NgClass, NgFor],
templateUrl: './star-rating.component.html',
styleUrl: './star-rating.component.css'
})
export class StarRatingComponent {
@Input() public rating: number = 0;
public getRatingArray(): number[] {
let stars = [];
for (let i = 0; i < 5; i++) {
if (i < this.rating) {
if (i + 0.5 == this.rating) {
stars.push(0.5);
} else {
stars.push(1);
}
} else {
stars.push(0);
}
}
return stars;
}
}

View File

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

View File

@ -0,0 +1,28 @@
import {Component, Input} from '@angular/core';
@Component({
selector: 'app-star',
standalone: true,
imports: [],
template: `
@for (_ of getList(); track null) {
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" color="yellow"><path fill="currentColor" d="m5.825 21l1.625-7.025L2 9.25l7.2-.625L12 2l2.8 6.625l7.2.625l-5.45 4.725L18.175 21L12 17.275z"/></svg>
}
@if (rating % 1 >= 0.5) {
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" color="yellow"><path fill="currentColor" d="M12 2L9.19 8.62L2 9.24l5.45 4.73L5.82 21L12 17.27z"/></svg>
}
`,
})
export class StarComponent {
@Input()
public rating: number = 0;
public getList(): null[] {
let rating = this.rating;
if (rating % 1 !== 0) {
rating = this.rating - 0.5;
}
return Array(rating).fill(null);
}
}

View File

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

View File

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

View File

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

View File

@ -5,10 +5,12 @@
<title>HotelManager</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.min.js" integrity="sha512-ykZ1QQr0Jy/4ZkvKuqWn4iF3lqPZyij9iRv6sGqLRdTPkY69YX6+7wvVGmsdBbiIfN/8OdsI7HABjvEok6ZopQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap-grid.min.css" integrity="sha512-i1b/nzkVo97VN5WbEtaPebBG8REvjWeqNclJ6AItj7msdVcaveKrlIIByDpvjk5nwHjXkIqGZscVxOrTb9tsMA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="./styles.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/material-design-icons/4.0.0/iconfont/material-icons.min.css" referrerpolicy="no-referrer" />
</head>
<body class="bg-pink-600 p-32">
<body class="bg-pink-600">
<app-root></app-root>
</body>
</html>

View File

@ -2,23 +2,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.button {
@apply border-black rounded px-3 py-1 border-[3px];
}
.input-field {
@apply border-red-500 text-3xl rounded-full bg-gray-500 font-bold p-3 m-3 border-8
}
.back-button {
@apply border-black rounded bg-blue-500 border-[30px] m-3 text-3xl font-bold p-3
}
.submit-button {
@apply border-black rounded-full bg-green-500 border-[30px] m-3 text-3xl font-bold p-3
}
.delete-button {
@apply border-black rounded bg-red-500 border-[30px] m-3 text-3xl font-bold p-3
}

View File

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

View File

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