diff --git a/package-lock.json b/package-lock.json index 0a05b92..0000198 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,9 @@ "@angular/material": "^20.2.9", "@angular/platform-browser": "^20.3.0", "@angular/router": "^20.3.0", - "@infinimotion/model-frontend": "^0.0.85", + "@infinimotion/model-frontend": "^0.0.89", "@tailwindcss/postcss": "^4.1.14", + "ngx-mask": "^20.0.3", "postcss": "^8.5.6", "rxjs": "~7.8.0", "tailwindcss": "^4.1.14", @@ -440,6 +441,7 @@ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.12.tgz", "integrity": "sha512-hz8GtiMy3N9/e8407ZfrByHD5GEC4SkWtxyUknWuTM9P88AOie0jDZ6CfQg9gQ0OJX+6BAbJV3RpYZA1uzNUqA==", "license": "MIT", + "peer": true, "dependencies": { "parse5": "^8.0.0", "tslib": "^2.3.0" @@ -490,6 +492,7 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.10.tgz", "integrity": "sha512-12fEzvKbEqjqy1fSk9DMYlJz6dF1MJVXuC5BB+oWWJpd+2lfh4xJ62pkvvLGAICI89hfM5n9Cy5kWnXwnqPZsA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -506,6 +509,7 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.10.tgz", "integrity": "sha512-cW939Lr8GZjPSYfbQKIDNrUaHWmn2M+zBbERThfq5skLuY+xM60bJFv4NqBekfX6YqKLCY62ilUZlnImYIXaqA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -519,6 +523,7 @@ "integrity": "sha512-9BemvpFxA26yIVdu8ROffadMkEdlk/AQQ2Jb486w7RPkrvUQ0pbEJukhv9aryJvhbMopT66S5H/j4ipOUMzmzQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "7.28.3", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -551,6 +556,7 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.10.tgz", "integrity": "sha512-g99Qe+NOVo72OLxowVF9NjCckswWYHmvO7MgeiZTDJbTjF9tXH96dMx7AWq76/GUinV10sNzDysVW16NoAbCRQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -576,6 +582,7 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.10.tgz", "integrity": "sha512-9yWr51EUauTEINB745AaHwZNTHLpXIm4uxuykxzOg+g2QskEgVfH26uS8G2ogdNuwYpB8wnsXWr34qhM3qgOWw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -611,6 +618,7 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.10.tgz", "integrity": "sha512-UV8CGoB5P3FmJciI3/I/n3L7C3NVgGh7bIlZ1BaB/qJDtv0Wq0rRAGwmT/Z3gwmrRtfHZWme7/CeQ2CYJmMyUQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -677,6 +685,7 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1392,9 +1401,9 @@ } }, "node_modules/@infinimotion/model-frontend": { - "version": "0.0.85", - "resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.85/model-frontend-0.0.85.tgz", - "integrity": "sha512-QPiZNl//Y1JdxtXk+VScc67h1K664z68PUCXRff9fRf4IHlYXtqutc+ainK8vxOVSqqL6EEmDAtbLsRwrG6kRg==", + "version": "0.0.89", + "resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.89/model-frontend-0.0.89.tgz", + "integrity": "sha512-lvvQy8RWs41Bz52uBgsUKkwn8teGlgxlmG8Rvsgkh+v1IMVWFWVQmfMS7Rznd0lCZRgK1ByihH80X9eAN12idA==", "license": "ISC" }, "node_modules/@inquirer/ansi": { @@ -1633,6 +1642,7 @@ "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@inquirer/checkbox": "^4.2.1", "@inquirer/confirm": "^5.1.14", @@ -3865,6 +3875,7 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4198,6 +4209,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -5294,6 +5306,7 @@ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -6245,7 +6258,8 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz", "integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/jiti": { "version": "2.6.1", @@ -6339,6 +6353,7 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -7055,6 +7070,7 @@ "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -7693,6 +7709,20 @@ "node": ">= 0.6" } }, + "node_modules/ngx-mask": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/ngx-mask/-/ngx-mask-20.0.3.tgz", + "integrity": "sha512-5bmrgbFGudj0mFN6cPv/TI+cFJxT4l61mLIFskdvaXsJL/Oj7thRmWYqvqHXjCboOcx8gT6T/Zypl5u9l2J8Jg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=14.0.0", + "@angular/core": ">=14.0.0", + "@angular/forms": ">=14.0.0" + } + }, "node_modules/node-addon-api": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", @@ -8712,6 +8742,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -8768,6 +8799,7 @@ "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -9616,7 +9648,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tuf-js": { "version": "3.1.0", @@ -9654,6 +9687,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9840,6 +9874,7 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -10243,6 +10278,7 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -10261,7 +10297,8 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "license": "MIT" + "license": "MIT", + "peer": true } } } diff --git a/package.json b/package.json index 5027aa3..0a846b3 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,9 @@ "@angular/material": "^20.2.9", "@angular/platform-browser": "^20.3.0", "@angular/router": "^20.3.0", - "@infinimotion/model-frontend": "^0.0.85", + "@infinimotion/model-frontend": "^0.0.89", "@tailwindcss/postcss": "^4.1.14", + "ngx-mask": "^20.0.3", "postcss": "^8.5.6", "rxjs": "~7.8.0", "tailwindcss": "^4.1.14", diff --git a/src/_theme-colors.scss b/src/_theme-colors.scss new file mode 100644 index 0000000..5c6e044 --- /dev/null +++ b/src/_theme-colors.scss @@ -0,0 +1,137 @@ +// This file was generated by running 'ng generate @angular/material:theme-color'. +// Proceed with caution if making changes to this file. + +@use 'sass:map'; +@use '@angular/material' as mat; + +// Note: Color palettes are generated from primary: 6366f1, tertiary: dd2979 +$_palettes: ( + primary: ( + 0: #000000, + 10: #07006c, + 20: #1000a9, + 25: #201cb4, + 30: #2f2ebe, + 35: #3c3dca, + 40: #494bd6, + 50: #6366f1, + 60: #8083ff, + 70: #a0a3ff, + 80: #c0c1ff, + 90: #e1e0ff, + 95: #f2efff, + 98: #fcf8ff, + 99: #fffbff, + 100: #ffffff, + ), + secondary: ( + 0: #000000, + 10: #13144a, + 20: #292a60, + 25: #34366c, + 30: #404178, + 35: #4b4d85, + 40: #575992, + 50: #7072ac, + 60: #8a8bc8, + 70: #a5a6e4, + 80: #c0c1ff, + 90: #e1e0ff, + 95: #f2efff, + 98: #fcf8ff, + 99: #fffbff, + 100: #ffffff, + ), + tertiary: ( + 0: #000000, + 10: #3f001c, + 20: #650031, + 25: #79003d, + 30: #8e0048, + 35: #a40054, + 40: #ba0060, + 50: #dd2979, + 60: #ff4993, + 70: #ff84ad, + 80: #ffb1c7, + 90: #ffd9e2, + 95: #ffecef, + 98: #fff8f8, + 99: #fffbff, + 100: #ffffff, + ), + neutral: ( + 0: #000000, + 10: #1b1b23, + 20: #303038, + 25: #3b3a44, + 30: #46464f, + 35: #52515b, + 40: #5e5d67, + 50: #777680, + 60: #918f9a, + 70: #acaab5, + 80: #c7c5d1, + 90: #e4e1ed, + 95: #f2effb, + 98: #fcf8ff, + 99: #fffbff, + 100: #ffffff, + 4: #0d0d15, + 6: #13131b, + 12: #1f1f27, + 17: #292932, + 22: #34343d, + 24: #393841, + 87: #dbd8e4, + 92: #e9e6f3, + 94: #efecf8, + 96: #f5f2fe, + ), + neutral-variant: ( + 0: #000000, + 10: #1a1a28, + 20: #2f2f3d, + 25: #3a3a49, + 30: #464554, + 35: #515160, + 40: #5d5d6d, + 50: #767586, + 60: #908fa0, + 70: #aba9bb, + 80: #c7c4d7, + 90: #e3e0f3, + 95: #f2efff, + 98: #fcf8ff, + 99: #fffbff, + 100: #ffffff, + ), + error: ( + 0: #000000, + 10: #410002, + 20: #690005, + 25: #7e0007, + 30: #93000a, + 35: #a80710, + 40: #ba1a1a, + 50: #de3730, + 60: #ff5449, + 70: #ff897d, + 80: #ffb4ab, + 90: #ffdad6, + 95: #ffedea, + 98: #fff8f7, + 99: #fffbff, + 100: #ffffff, + ), +); + +$_rest: ( + secondary: map.get($_palettes, secondary), + neutral: map.get($_palettes, neutral), + neutral-variant: map.get($_palettes, neutral-variant), + error: map.get($_palettes, error), +); + +$primary-palette: map.merge(map.get($_palettes, primary), $_rest); +$tertiary-palette: map.merge(map.get($_palettes, tertiary), $_rest); \ No newline at end of file diff --git a/src/app/app-module.ts b/src/app/app-module.ts index 3df7d00..f692e72 100644 --- a/src/app/app-module.ts +++ b/src/app/app-module.ts @@ -3,15 +3,16 @@ import { NgModule, provideBrowserGlobalErrorListeners } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserModule } from '@angular/platform-browser'; import { provideHttpClient, withFetch } from '@angular/common/http'; - - import { AppRoutingModule } from './app-routing-module'; import { App } from './app'; +import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask'; + import { MatIconModule } from '@angular/material/icon'; import { MatTabsModule } from '@angular/material/tabs'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatInputModule } from '@angular/material/input'; @@ -20,6 +21,7 @@ import { MatButtonModule, MatIconButton } from '@angular/material/button'; import { MatDividerModule } from '@angular/material/divider'; import { MatDialogClose, MatDialogTitle, MatDialogContent, MatDialogActions } from "@angular/material/dialog"; import { MatStepperModule } from '@angular/material/stepper'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { HeaderComponent } from './header/header.component'; import { HomeComponent } from './home/home.component'; @@ -28,7 +30,6 @@ import { MainLayoutComponent } from './layouts/main-layout/main-layout.component import { NavbarComponent } from './navbar/navbar.component'; import { PocModelComponent } from './poc-model-component/poc-model-component'; import { ScheduleComponent } from './schedule/schedule.component'; - import { MovieDurationComponent } from './movie-duration/movie-duration.component'; import { MoviePerformanceComponent } from './movie-performance/movie-performance.component'; import { MoviePosterComponent } from './movie-poster/movie-poster.component'; @@ -49,6 +50,11 @@ import { MovieImporterComponent } from './movie-importer/movie-importer.componen import { MovieImportNoSearchResultComponent } from './movie-import-no-search-result/movie-import-no-search-result.component'; import { MovieImportSearchInfoComponent } from './movie-import-search-info/movie-import-search-info.component'; import { LoginDialog } from './login/login.dialog'; +import { PerformanceInfoComponent } from './performance-info/performance-info.component'; +import { ShoppingCartComponent } from './shopping-cart/shopping-cart.component'; +import { OrderComponent } from './order/order.component'; +import { SeatSelectionComponent } from './seat-selection/seat-selection.component'; +import { NoSeatsInHallComponent } from './no-seats-in-hall/no-seats-in-hall.component'; @NgModule({ @@ -81,6 +87,11 @@ import { LoginDialog } from './login/login.dialog'; MovieImportNoSearchResultComponent, MovieImportSearchInfoComponent, LoginDialog, + PerformanceInfoComponent, + ShoppingCartComponent, + OrderComponent, + SeatSelectionComponent, + NoSeatsInHallComponent, ], imports: [ AppRoutingModule, @@ -92,6 +103,7 @@ import { LoginDialog } from './login/login.dialog'; MatTabsModule, MatToolbarModule, MatProgressBarModule, + MatProgressSpinnerModule, MatSnackBarModule, MatAutocompleteModule, MatInputModule, @@ -103,13 +115,17 @@ import { LoginDialog } from './login/login.dialog'; MatDialogTitle, MatDialogContent, MatDialogActions, - MatStepperModule -], + MatCheckboxModule, + MatStepperModule, + NgxMaskDirective, + NgxMaskPipe, + ], providers: [ provideBrowserGlobalErrorListeners(), provideHttpClient( withFetch(), - ) + ), + provideNgxMask(), ], bootstrap: [App] }) diff --git a/src/app/menu-header/menu-header.component.html b/src/app/menu-header/menu-header.component.html index 1941a29..beb2792 100644 --- a/src/app/menu-header/menu-header.component.html +++ b/src/app/menu-header/menu-header.component.html @@ -10,5 +10,13 @@ @if ( searchBar() ) { } + @if ( backToSchedule() ) { +
+ + arrow_back + Zurück zur Programmübersicht + +
+ } diff --git a/src/app/menu-header/menu-header.component.ts b/src/app/menu-header/menu-header.component.ts index 18155ff..4dd4ec2 100644 --- a/src/app/menu-header/menu-header.component.ts +++ b/src/app/menu-header/menu-header.component.ts @@ -12,4 +12,6 @@ export class MenuHeaderComponent { searchBar = input(false); movieSearchResult = output(); + + backToSchedule = input(false); } diff --git a/src/app/movie-category/movie-category.component.html b/src/app/movie-category/movie-category.component.html index 72757f5..3ff5e5c 100644 --- a/src/app/movie-category/movie-category.component.html +++ b/src/app/movie-category/movie-category.component.html @@ -1,3 +1,3 @@ - + {{ category() }} diff --git a/src/app/movie-duration/movie-duration.component.html b/src/app/movie-duration/movie-duration.component.html index 73845a3..b42ea62 100644 --- a/src/app/movie-duration/movie-duration.component.html +++ b/src/app/movie-duration/movie-duration.component.html @@ -1,4 +1,6 @@ - - + + @if (showIcon()) { + + } {{ durationText() }} diff --git a/src/app/movie-duration/movie-duration.component.ts b/src/app/movie-duration/movie-duration.component.ts index e029d3e..0bc94b6 100644 --- a/src/app/movie-duration/movie-duration.component.ts +++ b/src/app/movie-duration/movie-duration.component.ts @@ -8,9 +8,13 @@ import { Component, input, computed } from '@angular/core'; }) export class MovieDurationComponent { duration = input(0); + showIcon = input(true); durationText = computed(() => { if (this.duration() > 0) { + if (!this.showIcon()) { + return `${this.duration()} Minuten`; + } return `${this.duration()} Min.`; } return 'N/A'; diff --git a/src/app/movie-poster/movie-poster.component.html b/src/app/movie-poster/movie-poster.component.html index 1378eab..c038336 100644 --- a/src/app/movie-poster/movie-poster.component.html +++ b/src/app/movie-poster/movie-poster.component.html @@ -7,7 +7,7 @@ >
- - - + + +
diff --git a/src/app/movie-rating/movie-rating.component.html b/src/app/movie-rating/movie-rating.component.html index a8e4c76..9427cf9 100644 --- a/src/app/movie-rating/movie-rating.component.html +++ b/src/app/movie-rating/movie-rating.component.html @@ -1,3 +1,3 @@ - + {{ ratingText() }} diff --git a/src/app/movie-rating/movie-rating.component.ts b/src/app/movie-rating/movie-rating.component.ts index 60951ab..8dfb344 100644 --- a/src/app/movie-rating/movie-rating.component.ts +++ b/src/app/movie-rating/movie-rating.component.ts @@ -1,4 +1,4 @@ -import { Component, input, computed } from '@angular/core'; +import { Component, input, computed, HostBinding } from '@angular/core'; @Component({ selector: 'app-movie-rating', @@ -7,6 +7,11 @@ import { Component, input, computed } from '@angular/core'; styleUrl: './movie-rating.component.css' }) export class MovieRatingComponent { + + @HostBinding('class') get hostClasses(): string { + return this.ratingColor(); + } + rating = input(0); ratingColor = computed(() => { diff --git a/src/app/no-seats-in-hall/no-seats-in-hall.component.css b/src/app/no-seats-in-hall/no-seats-in-hall.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/no-seats-in-hall/no-seats-in-hall.component.html b/src/app/no-seats-in-hall/no-seats-in-hall.component.html new file mode 100644 index 0000000..08cea86 --- /dev/null +++ b/src/app/no-seats-in-hall/no-seats-in-hall.component.html @@ -0,0 +1,7 @@ +
+ + brightness_alert + +

Huch?! Keine Sitzplätze?

+

Hast du ein Glück, Stehplätze sind kostenlos.

+
diff --git a/src/app/no-seats-in-hall/no-seats-in-hall.component.ts b/src/app/no-seats-in-hall/no-seats-in-hall.component.ts new file mode 100644 index 0000000..bf6ce88 --- /dev/null +++ b/src/app/no-seats-in-hall/no-seats-in-hall.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-no-seats-in-hall', + standalone: false, + templateUrl: './no-seats-in-hall.component.html', + styleUrl: './no-seats-in-hall.component.css' +}) +export class NoSeatsInHallComponent { + +} diff --git a/src/app/order/order.component.css b/src/app/order/order.component.css new file mode 100644 index 0000000..367c809 --- /dev/null +++ b/src/app/order/order.component.css @@ -0,0 +1,19 @@ +mat-stepper { + background: transparent !important; +} +::ng-deep .mat-step-header { + background-color: transparent !important; +} + + +::ng-deep .mat-horizontal-stepper-header{ + pointer-events: none !important; +} + +.performance-info-space { + margin-top: calc(var(--spacing) * 24) +} + +::ng-deep .checkbox-invalid.mat-mdc-checkbox .mat-internal-form-field { + color: red !important; +} diff --git a/src/app/order/order.component.html b/src/app/order/order.component.html new file mode 100644 index 0000000..dce14e7 --- /dev/null +++ b/src/app/order/order.component.html @@ -0,0 +1,161 @@ +
+ +@if (loadingService.loading$ | async){ +
+ +
+} +@else if (performance()) { + +
+ +
+ + + + Warenkorb + +
+ + +
+ @for (seatCategory of seatCategories(); track $index) { +
+ + } + @empty { + + } +
+ + + + +
+

+ Tickets gesamt: +

+

+ {{ getPriceDisplay(totalPrice()) }} +

+
+ + +
+ + +
+
+ + +
+ Anschrift + +
+ + + + Name + + @if (fData['name'].hasError('minlength')) { Mindestens 3 Zeichen } + + + + + E-Mail Adresse + + @if (fData['email'].hasError('email')) { Ungültige E-Mail-Adresse } + + + +
+ + Ich akzeptiere die AGB und die Datenbestimmung + +
+ + +
+ + +
+
+
+ +
+ Zahlung + +
+ + + + Kartennummer + + @if (fPayment['cardNumber'].hasError('pattern')) { Ungültige Kartennummer } + + + + + + Kartenname + + @if (fPayment['cardName'].hasError('minlength')) { Mindestens 3 Zeichen } + + + +
+ + Gültig bis (MM/YY) + + @if (fPayment['expiry'].hasError('pattern')) { Ungültiges Format } + + + + CVV + + @if (fPayment['cvv'].hasError('pattern')) { 3–4 Ziffernt } + +
+ + +
+ + encrypted + +

+ Ihre Zahlung wird sicher über unsere Partner verarbeitet.
Wir speichern keine Zahlungsinformationen. +

+
+ + +
+ + +
+
+
+ +
+} +
diff --git a/src/app/order/order.component.ts b/src/app/order/order.component.ts new file mode 100644 index 0000000..f1c0989 --- /dev/null +++ b/src/app/order/order.component.ts @@ -0,0 +1,70 @@ +import { SelectedSeatsService } from './../selected-seats.service'; +import { LoadingService } from './../loading.service'; +import { Sitzkategorie, Vorstellung } from '@infinimotion/model-frontend'; +import { Component, computed, inject, input } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { StepperSelectionEvent } from '@angular/cdk/stepper'; + +@Component({ + selector: 'app-order', + standalone: false, + templateUrl: './order.component.html', + styleUrl: './order.component.css' +}) +export class OrderComponent { + paymentForm!: FormGroup; + dataForm!: FormGroup; + + submitted = false; + + constructor(private fb: FormBuilder) {} + + ngOnInit(): void { + this.paymentForm = this.fb.group({ + cardNumber: ['', [Validators.required, Validators.pattern(/^\d{16}$/)]], + cardName: ['', [Validators.required, Validators.minLength(3)]], + expiry: ['', [Validators.required, Validators.pattern(/^(0[1-9]|1[0-2])\/\d{2}$/)]], + cvv: ['', [Validators.required, Validators.pattern(/^\d{3,4}$/)]], + }); + this.dataForm = this.fb.group({ + name: ['', [Validators.required, Validators.minLength(3)]], + email: ['', [Validators.required, Validators.email]], + accept: ['', Validators.requiredTrue], + }); + } + + get fData() { return this.dataForm.controls; } + get fPayment() { return this.paymentForm.controls; } + + onSubmit() { + if (this.paymentForm.invalid) return; + console.log('Zahlungsdaten:', this.paymentForm.value); + } + + onStepChange(event: StepperSelectionEvent) { + this.submitted = false; + } + + stupidCheckboxWorkaround() { + this.submitted = true; + } + + performance = input(); + seatCategories = input.required(); + + loadingService = inject(LoadingService); + private selectedSeatsService = inject(SelectedSeatsService); + + totalPrice = computed(() => + this.selectedSeatsService.getSelectedSeatsList().reduce((sum, seat) => sum + seat.row.category.price, 0) + ); + + totalSeats = computed(() => + this.selectedSeatsService.getSelectedSeatsList().length + ); + + getPriceDisplay(price: number): string { + return `${(price / 100).toFixed(2)} €`; + } + +} diff --git a/src/app/performance-info/performance-info.component.css b/src/app/performance-info/performance-info.component.css new file mode 100644 index 0000000..28d36f0 --- /dev/null +++ b/src/app/performance-info/performance-info.component.css @@ -0,0 +1,3 @@ +.info-box { + color: var(--mat-sys-on-surface); +} diff --git a/src/app/performance-info/performance-info.component.html b/src/app/performance-info/performance-info.component.html new file mode 100644 index 0000000..786c373 --- /dev/null +++ b/src/app/performance-info/performance-info.component.html @@ -0,0 +1,21 @@ +
+ +
+ Movie Poster +
+ +
+

{{ getStartTimeString() }} • {{ performance().hall.name }}

+

{{ movie().title }}

+
+ + +
+
+ +
diff --git a/src/app/performance-info/performance-info.component.ts b/src/app/performance-info/performance-info.component.ts new file mode 100644 index 0000000..f648154 --- /dev/null +++ b/src/app/performance-info/performance-info.component.ts @@ -0,0 +1,30 @@ +import { Component, input } from '@angular/core'; +import { Vorstellung } from '@infinimotion/model-frontend'; + +@Component({ + selector: 'app-performance-info', + standalone: false, + templateUrl: './performance-info.component.html', + styleUrl: './performance-info.component.css' +}) +export class PerformanceInfoComponent { + performance = input.required(); + + getStartTimeString(): string { + const date = new Date(this.performance().start); + return date.toLocaleDateString('de-DE', { weekday: 'short' }) + '. ' + date.toLocaleDateString('de-DE') + ', ' + date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + ' Uhr'; + } + + movie() { + return this.performance().movie + } + + onPosterError(event: Event) { + const img = event.target as HTMLImageElement; + const placeholder = 'assets/poster_placeholder.png'; + + if (img.src !== window.location.origin + placeholder) { + img.src = placeholder; + } + } +} diff --git a/src/app/seat-selection/seat-selection.component.css b/src/app/seat-selection/seat-selection.component.css new file mode 100644 index 0000000..c206bd7 --- /dev/null +++ b/src/app/seat-selection/seat-selection.component.css @@ -0,0 +1,3 @@ +.seat-name { + color: var(--mat-sys-primary); +} diff --git a/src/app/seat-selection/seat-selection.component.html b/src/app/seat-selection/seat-selection.component.html new file mode 100644 index 0000000..53562eb --- /dev/null +++ b/src/app/seat-selection/seat-selection.component.html @@ -0,0 +1,11 @@ +

{{ seatCategory().name }}

+
+
+ + {{ seatCategory().icon }} + +

{{ getPriceDisplay(seatCategory().price) }}

+
+

× {{ selectedSeatsByCategory() }}

+

{{ getPriceDisplay(totalCategoryPrice()) }}

+
diff --git a/src/app/seat-selection/seat-selection.component.ts b/src/app/seat-selection/seat-selection.component.ts new file mode 100644 index 0000000..9c5ca14 --- /dev/null +++ b/src/app/seat-selection/seat-selection.component.ts @@ -0,0 +1,27 @@ +import { SelectedSeatsService } from './../selected-seats.service'; +import { Component, computed, inject, input } from '@angular/core'; +import { Sitzkategorie } from '@infinimotion/model-frontend'; + +@Component({ + selector: 'app-seat-selection', + standalone: false, + templateUrl: './seat-selection.component.html', + styleUrl: './seat-selection.component.css' +}) +export class SeatSelectionComponent { + seatCategory = input.required(); + + SelectedSeatsService = inject(SelectedSeatsService); + + selectedSeatsByCategory = computed(() => + this.SelectedSeatsService.getSelectedSeatsByCategory(this.seatCategory().id).length + ); + + totalCategoryPrice = computed(() => + this.selectedSeatsByCategory() * this.seatCategory().price + ); + + getPriceDisplay(price: number): string { + return `${(price / 100).toFixed(2)} €`; + } +} diff --git a/src/app/selected-seats.service.ts b/src/app/selected-seats.service.ts index b3d5c45..329ea0b 100644 --- a/src/app/selected-seats.service.ts +++ b/src/app/selected-seats.service.ts @@ -1,39 +1,37 @@ -import {Injectable} from '@angular/core'; +import { Injectable, signal } from '@angular/core'; import {Sitzplatz} from '@infinimotion/model-frontend'; @Injectable({ providedIn: 'root', }) export class SelectedSeatsService { - private selectedSeatsList: Sitzplatz[] = []; + private selectedSeatsSignal = signal([]); private seatIsSelectable: boolean = true; + get selectedSeats() { + return this.selectedSeatsSignal; + } + pushSelectedSeat(selectedSeat: Sitzplatz): void { - this.selectedSeatsList.push(selectedSeat); - //console.log("Added" + selectedSeat); - console.log(this.selectedSeatsList); + this.selectedSeatsSignal.update(seats => [...seats, selectedSeat]); } removeSelectedSeat(selectedSeat: Sitzplatz): void { - let removeId = this.selectedSeatsList.indexOf(selectedSeat); - - if(removeId !== -1) { - this.selectedSeatsList.splice(removeId, 1) - } - //console.log("Removed" + selectedSeat) - console.log(this.selectedSeatsList); + this.selectedSeatsSignal.update(seats => + seats.filter(seat => seat.id !== selectedSeat.id) + ); } getSelectedSeatsList(): Sitzplatz[] { - return this.selectedSeatsList; + return this.selectedSeatsSignal(); + } + + getSelectedSeatsByCategory(categoryId: number): Sitzplatz[] { + return this.selectedSeatsSignal().filter(seat => seat.row.category.id === categoryId); } clearSelectedSeatsList(): void { - this.selectedSeatsList = []; - //for (let i = this.selectedSeatsList.length - 1; i >= 0; i--) { - // const seat = this.selectedSeatsList[i]; - // this.removeSelectedSeat(seat); - //} + this.selectedSeatsSignal.set([]); } getSeatIsSelected(): boolean{ diff --git a/src/app/shopping-cart/shopping-cart.component.css b/src/app/shopping-cart/shopping-cart.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shopping-cart/shopping-cart.component.html b/src/app/shopping-cart/shopping-cart.component.html new file mode 100644 index 0000000..5aff33e --- /dev/null +++ b/src/app/shopping-cart/shopping-cart.component.html @@ -0,0 +1 @@ +

shopping-cart works!

diff --git a/src/app/shopping-cart/shopping-cart.component.ts b/src/app/shopping-cart/shopping-cart.component.ts new file mode 100644 index 0000000..382aba1 --- /dev/null +++ b/src/app/shopping-cart/shopping-cart.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-shopping-cart', + standalone: false, + templateUrl: './shopping-cart.component.html', + styleUrl: './shopping-cart.component.css' +}) +export class ShoppingCartComponent { + +} diff --git a/src/app/theater-layout/theater-layout.component.css b/src/app/theater-layout/theater-layout.component.css index e69de29..055c8c8 100644 --- a/src/app/theater-layout/theater-layout.component.css +++ b/src/app/theater-layout/theater-layout.component.css @@ -0,0 +1,4 @@ +:host { +border-radius: 0.5rem; + background-color: white; +} diff --git a/src/app/theater-layout/theater-layout.component.html b/src/app/theater-layout/theater-layout.component.html index a864e27..cd0560d 100644 --- a/src/app/theater-layout/theater-layout.component.html +++ b/src/app/theater-layout/theater-layout.component.html @@ -1,3 +1,10 @@ -@for (row of seatsPerRow(); track $index) { - -} +
+

+ Leinwand +

+
+
+ @for (row of seatsPerRow(); track $index) { + + } +
diff --git a/src/app/theater-overlay/theater-overlay.component.html b/src/app/theater-overlay/theater-overlay.component.html index bb9e257..c4c736e 100644 --- a/src/app/theater-overlay/theater-overlay.component.html +++ b/src/app/theater-overlay/theater-overlay.component.html @@ -1,9 +1,9 @@ -
-

- Leinwand -

-
-
- -
+ +
+ + + + + +
diff --git a/src/app/theater-overlay/theater-overlay.component.ts b/src/app/theater-overlay/theater-overlay.component.ts index b319bf8..362f1ed 100644 --- a/src/app/theater-overlay/theater-overlay.component.ts +++ b/src/app/theater-overlay/theater-overlay.component.ts @@ -1,8 +1,8 @@ import {Component, inject, OnInit} from '@angular/core'; import {HttpService} from '../http.service'; import {LoadingService} from '../loading.service'; -import {catchError, of, tap} from 'rxjs'; -import {Sitzplatz} from '@infinimotion/model-frontend'; +import {catchError, forkJoin, of, tap} from 'rxjs'; +import {Sitzkategorie, Sitzplatz, Vorstellung} from '@infinimotion/model-frontend'; import {TheaterSeatState} from '../model/theater-seat-state.model'; import {ActivatedRoute} from '@angular/router'; import {SelectedSeatsService} from '../selected-seats.service'; @@ -19,42 +19,62 @@ export class TheaterOverlayComponent implements OnInit { showId!: number; seatsPerRow: { seat: Sitzplatz, state: TheaterSeatState }[][] = [] + performance: Vorstellung | undefined; + seatCategories: Sitzkategorie[] = []; constructor(private route: ActivatedRoute, private selectedSeatService : SelectedSeatsService) {} - ngOnInit() { - this.showId = Number(this.route.snapshot.paramMap.get('id')!); - this.loadShowSeats(); - this.selectedSeatService.clearSelectedSeatsList(); - } +ngOnInit() { + this.showId = Number(this.route.snapshot.paramMap.get('id')!); + this.selectedSeatService.clearSelectedSeatsList(); + this.loadPerformanceAndSeats(); +} - loadShowSeats() { - this.loading.show(); - this.http.getSeatsByShowId(this.showId).pipe( - tap((data) => { - this.seatsPerRow = this.converter(data) - this.loading.hide(); - }), - catchError(err => { - this.loading.showError(err); - console.error('Fehler beim Laden der Vorstellung', err); - return of([]); - }) - ).subscribe(); - } +loadPerformanceAndSeats() { + this.loading.show(); + + forkJoin({ + performance: this.http.getPerformaceById(this.showId), + seats: this.http.getSeatsByShowId(this.showId) + }).pipe( + tap(({ performance, seats }) => { + this.performance = performance; + this.seatsPerRow = this.converter(seats); + this.loading.hide(); + }), + catchError(err => { + this.loading.showError(err); + console.error('Fehler beim Laden', err); + return of({ performance: null, seats: [] }); + }) + ).subscribe(); +} converter(resp: { seats: Sitzplatz[], reserved: Sitzplatz[], booked: Sitzplatz[] }): { seat: Sitzplatz, state: TheaterSeatState }[][] { let rows: { seat: Sitzplatz, state: TheaterSeatState }[][] = []; + const categoryMap = new Map(); + resp.seats.forEach(seat => { if (!rows[seat.row.position]) { rows[seat.row.position] = []; } - let state = resp.booked.find(other => other.id == seat.id) ? TheaterSeatState.BOOKED : resp.reserved.find(other => other.id == seat.id) ? TheaterSeatState.RESERVED : TheaterSeatState.AVAILABLE; + + let state = resp.booked.find(other => other.id == seat.id) ? TheaterSeatState.BOOKED + : resp.reserved.find(other => other.id == seat.id) ? TheaterSeatState.RESERVED + : TheaterSeatState.AVAILABLE; + rows[seat.row.position].push({seat: seat, state: state}); + + if (seat.row.category && !categoryMap.has(seat.row.category.id)) { + categoryMap.set(seat.row.category.id, seat.row.category); + } }); + + this.seatCategories = Array.from(categoryMap.values()).sort((a, b) => a.id - b.id); + rows = rows.filter(row => row.length > 0).sort((a, b) => a[0].seat.row.position - b[0].seat.row.position); rows.forEach(row => row.sort((a, b) => a.seat.position - b.seat.position)); return rows; diff --git a/src/custom-theme.scss b/src/custom-theme.scss index 5615bb5..640a19c 100644 --- a/src/custom-theme.scss +++ b/src/custom-theme.scss @@ -5,6 +5,33 @@ // Learn more about theming and how to use it for your application's // custom components at https://material.angular.dev/guide/theming @use '@angular/material' as mat; +@use './_theme-colors' as theme-colors; + +// Light Theme +html { + color-scheme: light; + @include mat.theme(( + color: ( + theme-type: light, + primary: theme-colors.$primary-palette, + tertiary: theme-colors.$tertiary-palette, + ), + )); +} + +// Dark Theme +html.dark { + color-scheme: dark; + @include mat.theme(( + color: ( + theme-type: dark, + primary: theme-colors.$primary-palette, + tertiary: theme-colors.$tertiary-palette, + ), + )); +} + + @include mat.progress-bar-overrides(( active-indicator-color: white, @@ -34,16 +61,9 @@ backdrop-filter: blur(2px); } - -html { - @include mat.theme(( - color: ( - primary: mat.$azure-palette, - tertiary: mat.$blue-palette, - ), - typography: Roboto, - density: 0, - )); +.mat-step-header .mat-step-icon:not(.mat-step-icon-selected):not(.mat-step-icon-completed):not(.mat-step-icon-state-edit) { + background-color: #bbb; + color: white; } body { diff --git a/src/index.html b/src/index.html index da193c0..2df19e0 100644 --- a/src/index.html +++ b/src/index.html @@ -8,6 +8,7 @@ +