diff --git a/angular.json b/angular.json index 735b501..66beaba 100644 --- a/angular.json +++ b/angular.json @@ -31,9 +31,7 @@ "builder": "@angular/build:application", "options": { "browser": "src/main.ts", - "polyfills": [ - "zone.js" - ], + "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "css", "assets": [ @@ -47,23 +45,20 @@ "output": "assets" } ], - "styles": [ - "src/custom-theme.scss", - "src/styles.css" - ] + "styles": ["src/custom-theme.scss", "src/styles.css"] }, "configurations": { "production": { "budgets": [ { "type": "initial", - "maximumWarning": "500kB", - "maximumError": "1MB" + "maximumWarning": "5MB", + "maximumError": "10MB" }, { "type": "anyComponentStyle", - "maximumWarning": "4kB", - "maximumError": "8kB" + "maximumWarning": "100kB", + "maximumError": "500kB" } ], "outputHashing": "all" @@ -94,10 +89,7 @@ "test": { "builder": "@angular/build:karma", "options": { - "polyfills": [ - "zone.js", - "zone.js/testing" - ], + "polyfills": ["zone.js", "zone.js/testing"], "tsConfig": "tsconfig.spec.json", "inlineStyleLanguage": "css", "assets": [ @@ -106,9 +98,7 @@ "input": "public" } ], - "styles": [ - "src/styles.css" - ] + "styles": ["src/styles.css"] } } } diff --git a/package-lock.json b/package-lock.json index d26398e..7e96782 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@angular/material": "^20.2.9", "@angular/platform-browser": "^20.3.0", "@angular/router": "^20.3.0", - "@infinimotion/model-frontend": "^0.0.102", + "@infinimotion/model-frontend": "^0.0.116", "@tailwindcss/postcss": "^4.1.14", "angularx-qrcode": "^20.0.0", "canvas-confetti": "^1.9.4", @@ -277,13 +277,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.2003.9", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2003.9.tgz", - "integrity": "sha512-p0GO2H8hiZjRHI9sm4tXTF3OpWaEnkqvB0GBGJfGp8RvpPfDA2t3j2NAUNtd75H+B0xdfyWLmNq9YJGpy6gznA==", + "version": "0.2003.10", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2003.10.tgz", + "integrity": "sha512-2SWetxJzS8gRX6OKQstkWx37VRvZVgcEBDLsDSaeTjpnwh81A+niZQjAVRdwL0NEt1Wixk/RxfeUuCmdyyHvhQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.3.9", + "@angular-devkit/core": "20.3.10", "rxjs": "7.8.2" }, "engines": { @@ -293,9 +293,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "20.3.9", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.9.tgz", - "integrity": "sha512-bXsAGIUb4p60x548YmvnMvjwd3FwWz6re1uTM7dV0XH8nQn3XMhOQ3Q3sAckzJHxkDuaRhB3K/a4kupoOmVfTQ==", + "version": "20.3.10", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.10.tgz", + "integrity": "sha512-COOT2eVebDwHhwENk12VR6m0wjL8D7p0dncEHF15zaBt1IXEnVhGESjSrs5klnPnt5T55qCBKyCTaeK7i/cS8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -321,13 +321,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "20.3.9", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.9.tgz", - "integrity": "sha512-oaIjAKPmHMZBTC0met5M7dbXBeZnCNwmHacT/kBHNVBAz/NI95fuAfb2P0Jxt7gWdQXejDSxWp0tL+sZIyO0xw==", + "version": "20.3.10", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.10.tgz", + "integrity": "sha512-2N2WF9lj+kr3uCG4+vFadYCL5hAT4dxMgzwScSdOqSd0O+GZD0CzKbDzlfvWIWC/ZealC5Sh4dFEQaRfmy72xA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.3.9", + "@angular-devkit/core": "20.3.10", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "8.2.0", @@ -340,14 +340,14 @@ } }, "node_modules/@angular/build": { - "version": "20.3.9", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-20.3.9.tgz", - "integrity": "sha512-Ulimvg6twPSCraaZECEmENfKBlD4M1yqeHlg6dCzFNM4xcwaGUnuG6O3cIQD59DaEvaG73ceM2y8ftYdxAwFow==", + "version": "20.3.10", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-20.3.10.tgz", + "integrity": "sha512-nQrj1nMNZygYDilThc7hPrD6/NIWF/BOSgMfE4VkXQp8d0QronP3HFJ/h77MeoughMRFRhix0pqQSlXJQ2SGTQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2003.9", + "@angular-devkit/architect": "0.2003.10", "@babel/core": "7.28.3", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", @@ -389,7 +389,7 @@ "@angular/platform-browser": "^20.0.0", "@angular/platform-server": "^20.0.0", "@angular/service-worker": "^20.0.0", - "@angular/ssr": "^20.3.9", + "@angular/ssr": "^20.3.10", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^20.0.0", @@ -439,11 +439,10 @@ } }, "node_modules/@angular/cdk": { - "version": "20.2.12", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.12.tgz", - "integrity": "sha512-hz8GtiMy3N9/e8407ZfrByHD5GEC4SkWtxyUknWuTM9P88AOie0jDZ6CfQg9gQ0OJX+6BAbJV3RpYZA1uzNUqA==", + "version": "20.2.13", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.13.tgz", + "integrity": "sha512-h1jTkCmJ/rEQQMkxgKFMCBOrMfjZEnppgdekNmSTerwdVp4vdosTDTzFH/kwiOGFeRClffmvqQ2XLG8mQOKOtA==", "license": "MIT", - "peer": true, "dependencies": { "parse5": "^8.0.0", "tslib": "^2.3.0" @@ -455,19 +454,19 @@ } }, "node_modules/@angular/cli": { - "version": "20.3.9", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.9.tgz", - "integrity": "sha512-4eKpRDg96B20yrKJqjA24zgxYy1RiRd70FvF/KG1hqSowsWwtzydtEJ3VM6iFWS9t1D8truuVpKjMEnn1Y274A==", + "version": "20.3.10", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.10.tgz", + "integrity": "sha512-CQzXScurBXSuMMn0jf6UYDItdggaM3bHYERKL4cUG1z5JqSozVFin1+TB1EjWYkddwdgC10R5xQurdMb+ahRNw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2003.9", - "@angular-devkit/core": "20.3.9", - "@angular-devkit/schematics": "20.3.9", + "@angular-devkit/architect": "0.2003.10", + "@angular-devkit/core": "20.3.10", + "@angular-devkit/schematics": "20.3.10", "@inquirer/prompts": "7.8.2", "@listr2/prompt-adapter-inquirer": "3.0.1", "@modelcontextprotocol/sdk": "1.17.3", - "@schematics/angular": "20.3.9", + "@schematics/angular": "20.3.10", "@yarnpkg/lockfile": "1.1.0", "algoliasearch": "5.35.0", "ini": "5.0.0", @@ -490,11 +489,10 @@ } }, "node_modules/@angular/common": { - "version": "20.3.10", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.10.tgz", - "integrity": "sha512-12fEzvKbEqjqy1fSk9DMYlJz6dF1MJVXuC5BB+oWWJpd+2lfh4xJ62pkvvLGAICI89hfM5n9Cy5kWnXwnqPZsA==", + "version": "20.3.12", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.12.tgz", + "integrity": "sha512-rFcDfe67ffrb435C6t2lc27WGbizeOcgce30tUhH0iezwEvU+kHHWezXXX6Ylx3TFgqGkhcxL0fliuFYrpM1Vw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -502,16 +500,15 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "20.3.10", + "@angular/core": "20.3.12", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "20.3.10", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.10.tgz", - "integrity": "sha512-cW939Lr8GZjPSYfbQKIDNrUaHWmn2M+zBbERThfq5skLuY+xM60bJFv4NqBekfX6YqKLCY62ilUZlnImYIXaqA==", + "version": "20.3.12", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.12.tgz", + "integrity": "sha512-bGESKz97nWiEQ/sydTq/Lzv3zlLvDb8t0msLG5Xti7Ch1EdLddXS8d2D/zFsjiGbAUKVsT6RgPCLHYoi4ocbhA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -520,12 +517,11 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "20.3.10", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.10.tgz", - "integrity": "sha512-9BemvpFxA26yIVdu8ROffadMkEdlk/AQQ2Jb486w7RPkrvUQ0pbEJukhv9aryJvhbMopT66S5H/j4ipOUMzmzQ==", + "version": "20.3.12", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.12.tgz", + "integrity": "sha512-3SJkexqsydYjIs0iLiJr5AdwkvumpzvjJM6s76iaxXHkRll5k/vM0wqkXLlSIwieBrecO9D4J73lDLWDevXl5A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.28.3", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -544,7 +540,7 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "20.3.10", + "@angular/compiler": "20.3.12", "typescript": ">=5.8 <6.0" }, "peerDependenciesMeta": { @@ -554,11 +550,10 @@ } }, "node_modules/@angular/core": { - "version": "20.3.10", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.10.tgz", - "integrity": "sha512-g99Qe+NOVo72OLxowVF9NjCckswWYHmvO7MgeiZTDJbTjF9tXH96dMx7AWq76/GUinV10sNzDysVW16NoAbCRQ==", + "version": "20.3.12", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.12.tgz", + "integrity": "sha512-K7vibMr55a7+EsuDhkg4Pk+ELuMm12olllwqL/CiQUcHXZ9Zgc4KYGTUuxWB69qJCG90gdSZS7tm5Dx0wDcyjg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -566,7 +561,7 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "20.3.10", + "@angular/compiler": "20.3.12", "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0" }, @@ -580,11 +575,10 @@ } }, "node_modules/@angular/forms": { - "version": "20.3.10", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.10.tgz", - "integrity": "sha512-9yWr51EUauTEINB745AaHwZNTHLpXIm4uxuykxzOg+g2QskEgVfH26uS8G2ogdNuwYpB8wnsXWr34qhM3qgOWw==", + "version": "20.3.12", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.12.tgz", + "integrity": "sha512-O0Jy8ScaN3qVipDfR4s0SIxGrz/+MbCdmR05ZYVWf1W5P3dvETKt9WNjX9fYYV47GdgSveyFjuCR2NvWlv94zA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -592,22 +586,22 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "20.3.10", - "@angular/core": "20.3.10", - "@angular/platform-browser": "20.3.10", + "@angular/common": "20.3.12", + "@angular/core": "20.3.12", + "@angular/platform-browser": "20.3.12", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/material": { - "version": "20.2.12", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-20.2.12.tgz", - "integrity": "sha512-DVenIZmV87qhDBlI2Xv3Z+b+IFI1s4wcZsFrzDi1FBMxKLsltJwMHf4SAmuqY0Mm/2Vw7HEZlfE130TuqjG8Ig==", + "version": "20.2.13", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-20.2.13.tgz", + "integrity": "sha512-9pjp2mULOxojYzOO7qdqt/gSVLrpYBwsIM3K0fxp+mNEcJgNjIxvmRKx46LY9+v0yrPY9puoQvP/T2C+o1+xsw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/cdk": "20.2.12", + "@angular/cdk": "20.2.13", "@angular/common": "^20.0.0 || ^21.0.0", "@angular/core": "^20.0.0 || ^21.0.0", "@angular/forms": "^20.0.0 || ^21.0.0", @@ -616,11 +610,10 @@ } }, "node_modules/@angular/platform-browser": { - "version": "20.3.10", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.10.tgz", - "integrity": "sha512-UV8CGoB5P3FmJciI3/I/n3L7C3NVgGh7bIlZ1BaB/qJDtv0Wq0rRAGwmT/Z3gwmrRtfHZWme7/CeQ2CYJmMyUQ==", + "version": "20.3.12", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.12.tgz", + "integrity": "sha512-14KQsXZyaQhbRwFz1W58CtbXQc9L+mfuHBgwQjQo99422Yk0ye5WVMb6DHH7dH671qFVqL0XL7zdOPBebaAnJQ==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -628,9 +621,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/animations": "20.3.10", - "@angular/common": "20.3.10", - "@angular/core": "20.3.10" + "@angular/animations": "20.3.12", + "@angular/common": "20.3.12", + "@angular/core": "20.3.12" }, "peerDependenciesMeta": { "@angular/animations": { @@ -639,9 +632,9 @@ } }, "node_modules/@angular/router": { - "version": "20.3.10", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.10.tgz", - "integrity": "sha512-Z03cfH1jgQ7XMDJj4R8qAGqivcvhdG3wYBwaiN1K1ODBgPhbFKNeD4stKqYp7xBNtswmM2O2jMxrL/Djwju4Gg==", + "version": "20.3.12", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.12.tgz", + "integrity": "sha512-hUipb9JI/Euy3bdlhzkcWlw3cTyssPTVTDwSvyGxWO4i+UKATQYmxh8EDOrDYzFp6Aexiy0Hff/H8umdsn6ZdA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -650,9 +643,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "20.3.10", - "@angular/core": "20.3.10", - "@angular/platform-browser": "20.3.10", + "@angular/common": "20.3.12", + "@angular/core": "20.3.12", + "@angular/platform-browser": "20.3.12", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -687,7 +680,6 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1403,9 +1395,9 @@ } }, "node_modules/@infinimotion/model-frontend": { - "version": "0.0.102", - "resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.102/model-frontend-0.0.102.tgz", - "integrity": "sha512-NJV9bSBubdOZ1GBIe9To3o/hh6AZscJcTyaZY2nGmMxH+GhtvO1AHmjhrQeRjwAKFiwZMEwEg4ktFiOAp3MTMQ==", + "version": "0.0.116", + "resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.116/model-frontend-0.0.116.tgz", + "integrity": "sha512-kGnZW1klIHzdL/44fOEUrDTVSkQSxErgHqwuSb6eQDLUW7Q9ZDL389LFR2haJCpqcYWIjsG2HjpvkJwrm2ctpA==", "license": "ISC" }, "node_modules/@inquirer/ansi": { @@ -1419,14 +1411,14 @@ } }, "node_modules/@inquirer/checkbox": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.1.tgz", - "integrity": "sha512-rOcLotrptYIy59SGQhKlU0xBg1vvcVl2FdPIEclUvKHh0wo12OfGkId/01PIMJ/V+EimJ77t085YabgnQHBa5A==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", "dev": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.1", + "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" @@ -1466,9 +1458,9 @@ } }, "node_modules/@inquirer/core": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.1.tgz", - "integrity": "sha512-hzGKIkfomGFPgxKmnKEKeA+uCYBqC+TKtRx5LgyHRCrF6S2MliwRIjp3sUaWwVzMp7ZXVs8elB0Tfe682Rpg4w==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", "dev": true, "license": "MIT", "dependencies": { @@ -1476,7 +1468,7 @@ "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", - "mute-stream": "^3.0.0", + "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" @@ -1494,13 +1486,13 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.22", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.22.tgz", - "integrity": "sha512-8yYZ9TCbBKoBkzHtVNMF6PV1RJEUvMlhvmS3GxH4UvXMEHlS45jFyqFy0DU+K42jBs5slOaA78xGqqqWAx3u6A==", + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.1", + "@inquirer/core": "^10.3.2", "@inquirer/external-editor": "^1.0.3", "@inquirer/type": "^3.0.10" }, @@ -1517,13 +1509,13 @@ } }, "node_modules/@inquirer/expand": { - "version": "4.0.22", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.22.tgz", - "integrity": "sha512-9XOjCjvioLjwlq4S4yXzhvBmAXj5tG+jvva0uqedEsQ9VD8kZ+YT7ap23i0bIXOtow+di4+u3i6u26nDqEfY4Q==", + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.1", + "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, @@ -1572,13 +1564,13 @@ } }, "node_modules/@inquirer/input": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.0.tgz", - "integrity": "sha512-h4fgse5zeGsBSW3cRQqu9a99OXRdRsNCvHoBqVmz40cjYjYFzcfwD0KA96BHIPlT7rZw0IpiefQIqXrjbzjS4Q==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.1", + "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "engines": { @@ -1594,13 +1586,13 @@ } }, "node_modules/@inquirer/number": { - "version": "3.0.22", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.22.tgz", - "integrity": "sha512-oAdMJXz++fX58HsIEYmvuf5EdE8CfBHHXjoi9cTcQzgFoHGZE+8+Y3P38MlaRMeBvAVnkWtAxMUF6urL2zYsbg==", + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.1", + "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "engines": { @@ -1616,14 +1608,14 @@ } }, "node_modules/@inquirer/password": { - "version": "4.0.22", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.22.tgz", - "integrity": "sha512-CbdqK1ioIr0Y3akx03k/+Twf+KSlHjn05hBL+rmubMll7PsDTGH0R4vfFkr+XrkB0FOHrjIwVP9crt49dgt+1g==", + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", "dev": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.1", + "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "engines": { @@ -1644,7 +1636,6 @@ "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.2.1", "@inquirer/confirm": "^5.1.14", @@ -1670,13 +1661,13 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.10.tgz", - "integrity": "sha512-Du4uidsgTMkoH5izgpfyauTL/ItVHOLsVdcY+wGeoGaG56BV+/JfmyoQGniyhegrDzXpfn3D+LFHaxMDRygcAw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.1", + "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, @@ -1693,13 +1684,13 @@ } }, "node_modules/@inquirer/search": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.1.tgz", - "integrity": "sha512-cKiuUvETublmTmaOneEermfG2tI9ABpb7fW/LqzZAnSv4ZaJnbEis05lOkiBuYX5hNdnX0Q9ryOQyrNidb55WA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.1", + "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" @@ -1717,14 +1708,14 @@ } }, "node_modules/@inquirer/select": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.1.tgz", - "integrity": "sha512-E9hbLU4XsNe2SAOSsFrtYtYQDVi1mfbqJrPDvXKnGlnRiApBdWMJz7r3J2Ff38AqULkPUD3XjQMD4492TymD7Q==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", "dev": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.1", + "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" @@ -3429,14 +3420,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "20.3.9", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.3.9.tgz", - "integrity": "sha512-XkgTwGhhrx+MVi2+TFO32d6Es5Uezzx7Y7B/e2ulDlj08bizxQj+9wkeLt5+bR8JWODHpEntZn/Xd5WvXnODGA==", + "version": "20.3.10", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.3.10.tgz", + "integrity": "sha512-F9ntS2CElpoWlENf4b03nwdTcN9Ri0Nb4SAE/pfRw3In09h2UHxYyf1ex9jqQt70xltDg4wvyuc3mMs+JlSx9A==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.3.9", - "@angular-devkit/schematics": "20.3.9", + "@angular-devkit/core": "20.3.10", + "@angular-devkit/schematics": "20.3.10", "jsonc-parser": "3.3.1" }, "engines": { @@ -3865,9 +3856,9 @@ "license": "MIT" }, "node_modules/@types/jasmine": { - "version": "5.1.12", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.12.tgz", - "integrity": "sha512-1BzPxNsFDLDfj9InVR3IeY0ZVf4o9XV+4mDqoCfyPkbsA7dYyKAPAb2co6wLFlHcvxPlt1wShm7zQdV7uTfLGA==", + "version": "5.1.13", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.13.tgz", + "integrity": "sha512-MYCcDkruFc92LeYZux5BC0dmqo2jk+M5UIZ4/oFnAPCXN9mCcQhLyj7F3/Za7rocVyt5YRr1MmqJqFlvQ9LVcg==", "dev": true, "license": "MIT" }, @@ -3877,7 +3868,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4097,9 +4087,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.8.26", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.26.tgz", - "integrity": "sha512-73lC1ugzwoaWCLJ1LvOgrR5xsMLTqSKIEoMHVtL9E/HNk0PXtTM76ZIm84856/SF7Nv8mPZxKoBsgpm0tR1u1Q==", + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz", + "integrity": "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4224,7 +4214,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -4412,9 +4401,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001754", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", - "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", + "version": "1.0.30001755", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", + "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", "dev": true, "funding": [ { @@ -4992,9 +4981,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.250", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.250.tgz", - "integrity": "sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==", + "version": "1.5.254", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz", + "integrity": "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==", "dev": true, "license": "ISC" }, @@ -5353,7 +5342,6 @@ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -6317,8 +6305,7 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz", "integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jiti": { "version": "2.6.1", @@ -6412,7 +6399,6 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -7129,7 +7115,6 @@ "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -7743,13 +7728,13 @@ } }, "node_modules/mute-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", - "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", "dev": true, "license": "ISC", "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/nanoid": { @@ -8991,7 +8976,6 @@ "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" } @@ -9048,7 +9032,6 @@ "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -9903,8 +9886,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tuf-js": { "version": "3.1.0", @@ -9942,7 +9924,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10129,7 +10110,6 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -10532,7 +10512,6 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -10551,8 +10530,7 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "license": "MIT", - "peer": true + "license": "MIT" } } } diff --git a/package.json b/package.json index 955f4ce..594376e 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@angular/material": "^20.2.9", "@angular/platform-browser": "^20.3.0", "@angular/router": "^20.3.0", - "@infinimotion/model-frontend": "^0.0.102", + "@infinimotion/model-frontend": "^0.0.116", "@tailwindcss/postcss": "^4.1.14", "angularx-qrcode": "^20.0.0", "canvas-confetti": "^1.9.4", diff --git a/src/app/app-module.ts b/src/app/app-module.ts index 0c4640a..3b6e3f1 100644 --- a/src/app/app-module.ts +++ b/src/app/app-module.ts @@ -23,6 +23,10 @@ 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 { MatBadgeModule } from '@angular/material/badge'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatTableModule } from '@angular/material/table'; import { HeaderComponent } from './header/header.component'; import { HomeComponent } from './home/home.component'; @@ -62,6 +66,8 @@ import { PurchaseSuccessComponent } from './purchase-success/purchase-success.co import { PurchaseFailedComponent } from './purchase-failed/purchase-failed.component'; import { TicketSmallComponent } from './ticket-small/ticket-small.component'; import { TicketListComponent } from './ticket-list/ticket-list.component'; +import { StatisticsComponent } from './statistics/statistics.component'; +import { ZoomWarningComponent } from './zoom-warning/zoom-warning.component'; import { PricelistComponent } from './pricelist/pricelist.component'; @@ -106,6 +112,8 @@ import { PricelistComponent } from './pricelist/pricelist.component'; PurchaseFailedComponent, TicketSmallComponent, TicketListComponent, + StatisticsComponent, + ZoomWarningComponent, PricelistComponent, ], imports: [ @@ -135,6 +143,10 @@ import { PricelistComponent } from './pricelist/pricelist.component'; NgxMaskDirective, NgxMaskPipe, QRCodeComponent, + MatBadgeModule, + MatTooltipModule, + MatPaginatorModule, + MatTableModule, ], providers: [ provideBrowserGlobalErrorListeners(), diff --git a/src/app/app-routing-module.ts b/src/app/app-routing-module.ts index 484a4b7..72de746 100644 --- a/src/app/app-routing-module.ts +++ b/src/app/app-routing-module.ts @@ -8,12 +8,13 @@ import { ScheduleComponent } from './schedule/schedule.component'; import { TheaterOverlayComponent} from './theater-overlay/theater-overlay.component'; import { MovieImporterComponent } from './movie-importer/movie-importer.component'; import { AuthGuard } from './auth.guard'; +import {StatisticsComponent} from './statistics/statistics.component'; import { PricelistComponent } from './pricelist/pricelist.component'; const routes: Routes = [ // Seiten ohne Layout { path: 'landing', component: HomeComponent }, - { path: 'poc-model', component: PocModelComponent }, + { path: 'poc-model', component: PocModelComponent, data: { allowMobile: true } }, // Seiten mit MainLayout { @@ -28,7 +29,13 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { roles: ['admin'] }, // Array von erlaubten Rollen. Derzeit gäbe es 'admin' und 'employee' }, - { path: 'selection/performance/:id', component: TheaterOverlayComponent}, + { path: 'performance/:performanceId/checkout', component: TheaterOverlayComponent}, + { + path: 'admin/statistics', + component: StatisticsComponent, + canActivate: [AuthGuard], + data: { roles: ['admin'] }, // Array von erlaubten Rollen. Derzeit gäbe es 'admin' und 'employee' + }, { path: 'prices', component: PricelistComponent }, ], }, diff --git a/src/app/app.html b/src/app/app.html index 67e7bd4..bc230ce 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1 +1,2 @@ + diff --git a/src/app/device-detection.service.ts b/src/app/device-detection.service.ts new file mode 100644 index 0000000..c4035ea --- /dev/null +++ b/src/app/device-detection.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class DeviceDetectionService { + private _isMobile: boolean; + + constructor() { + this._isMobile = this.checkIfMobile(); + } + + isMobile(): boolean { + return this._isMobile; + } + + private checkIfMobile(): boolean { + const userAgent = navigator.userAgent.toLowerCase(); + const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent); + const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; + const isSmallScreen = window.innerWidth < 768; + + return isMobileUA || (isTouchDevice && isSmallScreen); + } + + recheckDevice(): void { + this._isMobile = this.checkIfMobile(); + } +} diff --git a/src/app/http.service.ts b/src/app/http.service.ts index d189716..57c3684 100644 --- a/src/app/http.service.ts +++ b/src/app/http.service.ts @@ -1,4 +1,13 @@ -import { Kinosaal, Sitzplatz, Vorstellung, Film, OmdbSearch, Bestellung, Eintrittskarte } from '@infinimotion/model-frontend'; +import { + Kinosaal, + Sitzplatz, + Vorstellung, + Film, + OmdbSearch, + Bestellung, + Eintrittskarte, + StatisticsFilm, StatisticsVorstellung +} from '@infinimotion/model-frontend'; import { HttpClient } from "@angular/common/http"; import { inject, Injectable } from "@angular/core"; import { Observable } from "rxjs"; @@ -44,6 +53,12 @@ export class HttpService { } + /* POST /api/order-transaction/create */ + saveAddOrder(req: {order:Bestellung, tickets:Eintrittskarte[]}): Observable<{order:Bestellung, tickets:Eintrittskarte[]}> { + return this.http.post<{order: Bestellung, tickets: Eintrittskarte[]}>(`${this.baseUrl}order-transaction/create`, req); + } + + /* Eintrittskarte APIs */ /* GET /api/eintrittskarte/{id} */ @@ -154,8 +169,12 @@ export class HttpService { /* Show-Seats APIs */ /* GET /api/show-seats/{show} */ - getSeatsByShowId(show: number): Observable<{seats:Sitzplatz[], reserved:Sitzplatz[], booked:Sitzplatz[]}> { - return this.http.get<{seats:Sitzplatz[], reserved:Sitzplatz[], booked:Sitzplatz[]}>(`${this.baseUrl}show-seats/${show}`); + getSeatsByShowId(show: number): Observable<{ seats: Sitzplatz[], reserved: Sitzplatz[], booked: Sitzplatz[] }> { + return this.http.get<{ + seats: Sitzplatz[], + reserved: Sitzplatz[], + booked: Sitzplatz[] + }>(`${this.baseUrl}show-seats/${show}`); } @@ -164,12 +183,25 @@ export class HttpService { /* GET /api/importer/search */ searchMovie(query: string): Observable { return this.http.get(`${this.baseUrl}importer/search`, { - params: { title: query } + params: {title: query} }); } /* POST /api/importer/import */ importMovie(imdbId: string): Observable { - return this.http.post(`${this.baseUrl}importer/import?id=${imdbId}`, {}) + return this.http.post(`${this.baseUrl}importer/import?id=${imdbId}`, {}) + } + + + /* Statistics APIs */ + + /* GET /api/statistics/movies */ + getMovieStatistics(): Observable { + return this.http.get(`${this.baseUrl}statistics/movies`) + } + + /* GET /api/statistics/shows */ + getShowStatistics(): Observable { + return this.http.get(`${this.baseUrl}statistics/shows`) } } diff --git a/src/app/main/main.component.html b/src/app/main/main.component.html index 9979e51..20d8c07 100644 --- a/src/app/main/main.component.html +++ b/src/app/main/main.component.html @@ -1,11 +1,11 @@ -
-
+
+

Willkommen bei

-
+
 InfiniMotion

! 🎬

@@ -41,7 +41,7 @@ Wir haben uns bei Gestaltung und Stil bewusst an bestehenden Kinowebsites orientiert. Dabei handelt es sich um eine rein stilistische Anlehnung; diese Seite verfolgt keinerlei kommerzielle Zwecke und dient ausschließlich universitären Zwecken. Marken, Designs oder Funktionalitäten, die bekannten Anbietern ähneln, sind nicht als Kopie zum Wettbewerb gedacht, sondern als pragmatische Inspirationsquelle im Rahmen der Praxisarbeit. - + https://infinimotion.de wird zum Projektende offline genommen. @@ -52,57 +52,57 @@
-
- +
+ - - - Sprint #0:    Planung, Installation und Vorbereitung - - + + + Sprint #0:    Planung, Installation und Vorbereitung + + - - - Sprint #1:    Programmübersicht - - + + + Sprint #1:    Programmübersicht + + - - - Sprint #2:    Kinosäle anzeigen - - + + + Sprint #2:    Kinosäle anzeigen + + - - - Sprint #3:    Vorstellungstickets reservieren und buchen - - + + + Sprint #3:    Vorstellungstickets reservieren und buchen + + - - - Sprint #4:    Statistiken auswerten und anzeigen - - + + + Sprint #4:    Statistiken auswerten und anzeigen + + - - - Sprint #5:    Aufbereitung und Optimierung - - - - + + + Sprint #5:    Aufbereitung und Optimierung + + + +
diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index 706cf1a..8bfe389 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -7,7 +7,7 @@ import { Component } from '@angular/core'; styleUrl: './main.component.css' }) export class MainComponent { - currentSprint = 3; + currentSprint = 4; isCompleted(index: number): boolean { return index <= this.currentSprint; diff --git a/src/app/menu-header/menu-header.component.html b/src/app/menu-header/menu-header.component.html index beb2792..a571725 100644 --- a/src/app/menu-header/menu-header.component.html +++ b/src/app/menu-header/menu-header.component.html @@ -3,8 +3,8 @@ @if ( icon() ) { {{ icon() }} } -

- {{ title() }} +

+ {{ label() }}

@if ( searchBar() ) { diff --git a/src/app/menu-header/menu-header.component.ts b/src/app/menu-header/menu-header.component.ts index 4dd4ec2..ea79579 100644 --- a/src/app/menu-header/menu-header.component.ts +++ b/src/app/menu-header/menu-header.component.ts @@ -7,7 +7,7 @@ import { Component, input, output } from '@angular/core'; styleUrl: './menu-header.component.css' }) export class MenuHeaderComponent { - title = input.required(); + label = input.required(); icon = input(); searchBar = input(false); diff --git a/src/app/movie-importer/movie-importer.component.html b/src/app/movie-importer/movie-importer.component.html index 42b2ea2..6f614b8 100644 --- a/src/app/movie-importer/movie-importer.component.html +++ b/src/app/movie-importer/movie-importer.component.html @@ -1,4 +1,4 @@ - +
diff --git a/src/app/movie-performance/movie-performance.component.html b/src/app/movie-performance/movie-performance.component.html index f1811e4..1584f95 100644 --- a/src/app/movie-performance/movie-performance.component.html +++ b/src/app/movie-performance/movie-performance.component.html @@ -1,6 +1,6 @@ -
+

{{ hall() }}

diff --git a/src/app/movie-performance/movie-performance.component.ts b/src/app/movie-performance/movie-performance.component.ts index b6925d0..6f98ff9 100644 --- a/src/app/movie-performance/movie-performance.component.ts +++ b/src/app/movie-performance/movie-performance.component.ts @@ -15,7 +15,7 @@ export class MoviePerformanceComponent implements OnInit { route: string = ''; ngOnInit() { - this.route = `../selection/performance/${this.id()}`; + this.route = `../performance/${this.id()}/checkout`; } startTime = computed(() => diff --git a/src/app/movie-search/movie-search.component.html b/src/app/movie-search/movie-search.component.html index add7471..7b0f9b6 100644 --- a/src/app/movie-search/movie-search.component.html +++ b/src/app/movie-search/movie-search.component.html @@ -1,16 +1,22 @@ - - - Film suchen - +
- - - + @if (searchControl.value && searchControl.value.length > 0) { + + } - - @for (option of filteredOptions | async; track option) { - {{option}} - } - - - +
+ + Film suchen + + + + @for (option of filteredOptions | async; track option) { + {{option}} + } + + +
+ +
diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index cdf7f7a..a8659a7 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -12,6 +12,7 @@ export class NavbarComponent { {label: 'Programm', path: '/schedule'}, {label: 'Preise', path: '/prices'}, {label: 'Film importieren', path: '/admin/movie-importer'}, + {label: 'Statistiken', path: '/admin/statistics'}, ] private auth = inject(AuthService) diff --git a/src/app/order/order.component.ts b/src/app/order/order.component.ts index 8b13379..868029e 100644 --- a/src/app/order/order.component.ts +++ b/src/app/order/order.component.ts @@ -161,37 +161,31 @@ export class OrderComponent { submitOrder(order: Bestellung, seats: Sitzplatz[], performance: Vorstellung, mode: SubmissionMode) { - this.httpService.addOrder(order).pipe( - // Order erstellen - switchMap(createdOrder => { - // Tickets parallel erstellen - const ticketObservables = seats.map(seat => { - const ticket = this.generateNewTicketObject(performance, seat, createdOrder); - return this.httpService.addTicket(ticket); - }); + // Tickets anlegen + const tickets = seats.map(seat => { + return this.generateNewTicketObject(performance, seat, order); + }); - // Warten bis alles fertig sind - return forkJoin(ticketObservables).pipe( - tap(createdTickets => { - // Success Handling - if (mode === 'reservation') { - this.orderState.set({ - status: 'reservation-success', - order: createdOrder - }); - } else { - this.orderState.set({ - status: 'purchase-success', - tickets: createdTickets - }); - } + // Transaktionssicher Sitzplatzbuchung + this.httpService.saveAddOrder({order, tickets}).pipe( + tap(createdOrderAndTickets => { + // Success Handling + if (mode === 'reservation') { + this.orderState.set({ + status: 'reservation-success', + order: createdOrderAndTickets.order + }); + } else { + this.orderState.set({ + status: 'purchase-success', + tickets: createdOrderAndTickets.tickets + }); + } - this.selectedSeatsService.commit(); - this.loadingService.hide(); - this.showConfetti(); - }) - ); + this.selectedSeatsService.commit(); + this.loadingService.hide(); + this.showConfetti(); }), catchError(err => { // Error handling diff --git a/src/app/performance-info/performance-info.component.html b/src/app/performance-info/performance-info.component.html index 786c373..ee30310 100644 --- a/src/app/performance-info/performance-info.component.html +++ b/src/app/performance-info/performance-info.component.html @@ -13,7 +13,7 @@

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

{{ movie().title }}

- +
diff --git a/src/app/purchase-failed/purchase-failed.component.html b/src/app/purchase-failed/purchase-failed.component.html index 6cc5888..d841ce0 100644 --- a/src/app/purchase-failed/purchase-failed.component.html +++ b/src/app/purchase-failed/purchase-failed.component.html @@ -6,6 +6,6 @@

Leider konnten Ihre Sitzplätze nicht gekauft werden. Dies kann passieren, wenn andere Nutzer zeitgleich versucht haben, dieselben Sitzplätze zu kaufen.

- +
diff --git a/src/app/purchase-success/purchase-success.component.html b/src/app/purchase-success/purchase-success.component.html index 4d40726..c439043 100644 --- a/src/app/purchase-success/purchase-success.component.html +++ b/src/app/purchase-success/purchase-success.component.html @@ -2,10 +2,10 @@

Vielen Dank für Ihren Einkauf!

Ihre Sitzplätze wurden erfolgreich gebucht.

- + - - + +
diff --git a/src/app/reservation-failed/reservation-failed.component.html b/src/app/reservation-failed/reservation-failed.component.html index a8913c0..abd1532 100644 --- a/src/app/reservation-failed/reservation-failed.component.html +++ b/src/app/reservation-failed/reservation-failed.component.html @@ -6,6 +6,6 @@

Leider konnten Ihre Sitzplätze nicht reserviert werden. Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, dieselben Sitzplätze zu reservieren.

- +
diff --git a/src/app/reservation-success/reservation-success.component.html b/src/app/reservation-success/reservation-success.component.html index 0a463a0..8d28e59 100644 --- a/src/app/reservation-success/reservation-success.component.html +++ b/src/app/reservation-success/reservation-success.component.html @@ -1,17 +1,15 @@

Reservierung erfolgreich!

-

Ihre Sitzplätze wurden erfolgreich reserviert. Bitte nennen sie den folgenden Code an der Kasse, um Ihre Reservierung in eine Buchung umzuwandeln.

{{ order().code }}
- - -
+ + +
Reservierung stornieren
- diff --git a/src/app/schedule/schedule.component.css b/src/app/schedule/schedule.component.css index e80c311..239f2e2 100644 --- a/src/app/schedule/schedule.component.css +++ b/src/app/schedule/schedule.component.css @@ -5,3 +5,7 @@ ::ng-deep .mat-mdc-tab .mdc-tab-indicator__content--underline { border-color: #6366f1 !important; /* indigo-500 */ } + +.mat-badge-content { + background: #dd2979; +} diff --git a/src/app/schedule/schedule.component.html b/src/app/schedule/schedule.component.html index 902dab4..84b6683 100644 --- a/src/app/schedule/schedule.component.html +++ b/src/app/schedule/schedule.component.html @@ -1,20 +1,25 @@ - + @for (dateInfo of dates; track dateInfo.date; let i = $index) { + + + {{ dateInfo.label }} + + @if (getMovieCount(i) > 0) { - @if (hasSearchResults(i)) { - @for (group of dateInfo.performances; track group.movie.id) { - @if (group.movie.title.toLowerCase().includes(movieSearchResult.toLowerCase())) { - - } + @for (group of dateInfo.performances; track group.movie.id) { + @if (group.movie.title.toLowerCase().includes(movieSearchResult.toLowerCase())) { + } - } @else { - } } @else { - + @if (isSearch()) { + + } @else { + + } } } diff --git a/src/app/schedule/schedule.component.ts b/src/app/schedule/schedule.component.ts index 5d88ada..cd58a67 100644 --- a/src/app/schedule/schedule.component.ts +++ b/src/app/schedule/schedule.component.ts @@ -31,15 +31,7 @@ export class ScheduleComponent implements OnInit { this.loadPerformances(this.bookableDays); } - hasSearchResults(dateIndex: number): boolean { - if (!this.movieSearchResult) return true; - - return this.dates[dateIndex].performances.some(group => - group.movie.title.toLowerCase().includes(this.movieSearchResult.toLowerCase()) - ); - } - - generateDates(bookableDays: number) { + private generateDates(bookableDays: number) { const today = new Date(); for (let i = 0; i < bookableDays; i++) { const date = new Date(today); @@ -58,7 +50,7 @@ export class ScheduleComponent implements OnInit { } } - loadPerformances(bookableDays: number) { + private loadPerformances(bookableDays: number) { this.loading.show(); const filter = this.generateDateFilter(bookableDays); this.http.getPerformacesByFilter(filter).pipe( @@ -75,22 +67,21 @@ export class ScheduleComponent implements OnInit { ).subscribe(); } -private generateDateFilter(bookableDays: number): string[] { - const startDate = new Date(); - const endDate = new Date(); - endDate.setDate(startDate.getDate() + bookableDays - 1); + private generateDateFilter(bookableDays: number): string[] { + const startDate = new Date(); + const endDate = new Date(); + endDate.setDate(startDate.getDate() + bookableDays - 1); - const startStr = startDate.toISOString().split('T')[0] + 'T00:00:00'; - const endStr = endDate.toISOString().split('T')[0] + 'T23:59:59'; + const startStr = startDate.toISOString().split('T')[0] + 'T00:00:00'; + const endStr = endDate.toISOString().split('T')[0] + 'T23:59:59'; - return [ - `ge;start;date;${startStr}`, - `le;start;date;${endStr}`, - ]; -} + return [ + `ge;start;date;${startStr}`, + `le;start;date;${endStr}`, + ]; + } - - assignPerformancesToDates() { + private assignPerformancesToDates() { // Gruppieren nach Datum const groupedByDate: { [key: string]: Vorstellung[] } = {}; @@ -133,6 +124,22 @@ private generateDateFilter(bookableDays: number): string[] { } getMovieCount(index: number): number { - return this.dates[index].performances.length; + if (!this.dates[index]?.performances) { + return 0; + } + + const performances = this.dates[index].performances; + + if (!this.isSearch()) { + return performances.length; + } + + return performances.filter(group => + group.movie.title.toLowerCase().includes(this.movieSearchResult.toLowerCase()) + ).length; + } + + isSearch(): boolean { + return !!this.movieSearchResult && this.movieSearchResult.trim() !== ''; } } diff --git a/src/app/seat-row/seat-row.component.css b/src/app/seat-row/seat-row.component.css index 8b13789..4cf8c8e 100644 --- a/src/app/seat-row/seat-row.component.css +++ b/src/app/seat-row/seat-row.component.css @@ -1 +1,6 @@ - +.empty-seat-space { + width: 30px; + /* Keine Ahnung, wo die zusätzlichen 6.5px herkommen müssen. Wir sonst dünner angezeigt */ + height: 36.5px; + /* height: 30px; */ +} diff --git a/src/app/seat-row/seat-row.component.html b/src/app/seat-row/seat-row.component.html index 869b536..5ef657e 100644 --- a/src/app/seat-row/seat-row.component.html +++ b/src/app/seat-row/seat-row.component.html @@ -1,3 +1,8 @@ @for (entry of rowSeatList(); track $index) { - + + @if (entry.seat != null && entry.state != null) { + + } @else { +
+ } } diff --git a/src/app/seat-row/seat-row.component.ts b/src/app/seat-row/seat-row.component.ts index ec4c001..6afab92 100644 --- a/src/app/seat-row/seat-row.component.ts +++ b/src/app/seat-row/seat-row.component.ts @@ -9,5 +9,5 @@ import {TheaterSeatState} from '../model/theater-seat-state.model'; styleUrl: './seat-row.component.css' }) export class SeatRowComponent { - rowSeatList = input.required<{ seat: Sitzplatz, state: TheaterSeatState }[]>(); + rowSeatList = input.required<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[]>(); } diff --git a/src/app/statistics/statistics.component.css b/src/app/statistics/statistics.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/statistics/statistics.component.html b/src/app/statistics/statistics.component.html new file mode 100644 index 0000000..4ab7190 --- /dev/null +++ b/src/app/statistics/statistics.component.html @@ -0,0 +1,85 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ID{{row.movieId}}Titel{{row.movieTitle}} + Umsatz + {{(row.earnings/100).toFixed(2)}} € + Gebuchte Tickets + {{row.tickets}}
+
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID{{row.showId}}Kinosaal{{row.showHallName}}Film Name{{row.movieTitle}}Datum{{formatDate(row.showStart)}} + Umsatz + {{(row.earnings/100).toFixed(2)}} € + Gebuchte Tickets + {{row.tickets}}
+
+ + + diff --git a/src/app/statistics/statistics.component.ts b/src/app/statistics/statistics.component.ts new file mode 100644 index 0000000..acc4b38 --- /dev/null +++ b/src/app/statistics/statistics.component.ts @@ -0,0 +1,56 @@ +import {Component, inject} from '@angular/core'; +import {HttpService} from '../http.service'; +import { + StatisticsFilm, + StatisticsVorstellung, +} from '@infinimotion/model-frontend'; +import {LoadingService} from '../loading.service'; +import {firstValueFrom, forkJoin} from 'rxjs'; + +@Component({ + selector: 'app-statistics', + standalone: false, + templateUrl: './statistics.component.html', + styleUrl: './statistics.component.css', +}) +export class StatisticsComponent { + private http = inject(HttpService); + protected movies: StatisticsFilm[] = []; + protected shows: StatisticsVorstellung[] = []; + protected moviesDisplayedColumns: string[] = ['id', 'title', 'earnings', 'tickets']; + protected showsDisplayedColumns: string[] = ['id', 'hall', 'movie_title', 'date', 'earnings', 'tickets']; + protected movieResultsLength: number = 0; + protected showsResultLength: number = 0; + + private loading = inject(LoadingService); + + ngOnInit(): void { + this.loading.show() + this.loadData().then(); + } + + async loadData() { + let movieRequest = this.http.getMovieStatistics(); + let showRequest = this.http.getShowStatistics(); + let movieResponse = await firstValueFrom(movieRequest); + let showResponse = await firstValueFrom(showRequest); + this.movies = movieResponse + this.shows = showResponse + if (this.movies.length / 30 < 1) { + this.movieResultsLength = 1; + } else { + this.movieResultsLength = Math.ceil(this.movies.length / 30); + } + if (this.shows.length / 30 < 1) { + this.showsResultLength = 1; + } else { + this.showsResultLength = Math.ceil(this.shows.length / 30); + } + this.loading.hide(); + } + + formatDate(date: Date) { + return new Date(date).toLocaleString("de"); + } + +} diff --git a/src/app/theater-layout/theater-layout.component.html b/src/app/theater-layout/theater-layout.component.html index 5637093..0563b21 100644 --- a/src/app/theater-layout/theater-layout.component.html +++ b/src/app/theater-layout/theater-layout.component.html @@ -3,12 +3,12 @@ Leinwand

-
+
@for (row of seatsPerRow(); track $index) {
-
+
@if ($index % 4 === 0) { speaker @@ -25,7 +25,7 @@ -
+
@if ($index % 4 === 0) { speaker diff --git a/src/app/theater-layout/theater-layout.component.ts b/src/app/theater-layout/theater-layout.component.ts index d05fbce..1478459 100644 --- a/src/app/theater-layout/theater-layout.component.ts +++ b/src/app/theater-layout/theater-layout.component.ts @@ -10,7 +10,7 @@ import {TheaterSeatState} from '../model/theater-seat-state.model'; styleUrl: './theater-layout.component.css' }) export class TheaterLayoutComponent { - seatsPerRow = input.required<{ seat: Sitzplatz, state: TheaterSeatState }[][]>(); + seatsPerRow = input.required<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>(); protected selectedSeatsService = inject(SelectedSeatsService); } diff --git a/src/app/theater-overlay/theater-overlay.component.html b/src/app/theater-overlay/theater-overlay.component.html index f2d7a36..78bd5b8 100644 --- a/src/app/theater-overlay/theater-overlay.component.html +++ b/src/app/theater-overlay/theater-overlay.component.html @@ -1,8 +1,8 @@ - + -
+
-
+
@if (!performance && (loading.loading$ | async)){
@@ -18,6 +18,6 @@
- +
diff --git a/src/app/theater-overlay/theater-overlay.component.ts b/src/app/theater-overlay/theater-overlay.component.ts index 5a3e55c..f588044 100644 --- a/src/app/theater-overlay/theater-overlay.component.ts +++ b/src/app/theater-overlay/theater-overlay.component.ts @@ -25,7 +25,8 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { readonly loading = inject(LoadingService); showId!: number; - seatsPerRow = signal<{ seat: Sitzplatz, state: TheaterSeatState }[][]>([]); + orderId?: string; + seatsPerRow = signal<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>([]); performance: Vorstellung | undefined; seatCategories: Sitzkategorie[] = []; @@ -33,7 +34,8 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { private isInitialLoad = signal(true); ngOnInit() { - this.showId = Number(this.route.snapshot.paramMap.get('id')!); + this.showId = Number(this.route.snapshot.paramMap.get('performanceId')!); + this.orderId = this.route.snapshot.queryParams['paramName']; this.selectedSeatService.clearSelection(); this.selectedSeatService.setSeatSelectable(true); @@ -90,12 +92,13 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { } converter(resp: { seats: Sitzplatz[], reserved: Sitzplatz[], booked: Sitzplatz[] }): { - seat: Sitzplatz, - state: TheaterSeatState + seat: Sitzplatz | null, + state: TheaterSeatState | null }[][] { - let rows: { seat: Sitzplatz, state: TheaterSeatState }[][] = []; + let rows: { seat: Sitzplatz | null, state: TheaterSeatState | null }[][] = []; const categoryMap = new Map(); + // Sitzplätze sammeln resp.seats.forEach(seat => { if (!rows[seat.row.position]) { rows[seat.row.position] = []; @@ -114,9 +117,50 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { 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; + rows = rows.filter(row => row && row.length > 0).sort((a, b) => a[0].seat!.row.position - b[0].seat!.row.position); + + if (rows.length === 0) { + return []; + } + + // Leere Plätze auffüllen + const filledSeats: { seat: Sitzplatz | null, state: TheaterSeatState | null }[][] = []; + + rows.forEach(row => { + row.sort((a, b) => a.seat!.position - b.seat!.position) + + const minPos = row[0].seat!.position; + const maxPos = row[row.length - 1].seat!.position; + const filledRow: { seat: Sitzplatz | null, state: TheaterSeatState | null }[] = []; + + for (let pos = minPos; pos <= maxPos; pos++) { + const existingSeat = row.find(s => s.seat!.position === pos); + if (existingSeat) { + filledRow.push(existingSeat); + } else { + filledRow.push({ seat: null, state: null }); + } + } + + filledSeats.push(filledRow); + }); + + // Leere Reihen auffüllen + const minRowPos = rows[0][0].seat!.row.position; + const maxRowPos = rows[rows.length - 1][0].seat!.row.position; + const filledRows: { seat: Sitzplatz | null, state: TheaterSeatState | null }[][] = []; + + let processedIndex = 0; + for (let rowPos = minRowPos; rowPos <= maxRowPos; rowPos++) { + if (processedIndex < filledSeats.length && filledSeats[processedIndex][0].seat!.row.position === rowPos) { + filledRows.push(filledSeats[processedIndex]); + processedIndex++; + } else { + filledRows.push([{ seat: null, state: null }]); + } + } + + return filledRows; } refreshSeats(): void { diff --git a/src/app/zoom-detection.service.ts b/src/app/zoom-detection.service.ts new file mode 100644 index 0000000..d9e3311 --- /dev/null +++ b/src/app/zoom-detection.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, fromEvent } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class ZoomDetectionService { + private zoomLevel$ = new BehaviorSubject(this.getZoomLevel()); + + constructor() { + // Zoom-Änderungen überwachen + fromEvent(window, 'resize') + .pipe(debounceTime(200)) + .subscribe(() => { + this.zoomLevel$.next(this.getZoomLevel()); + }); + } + + getZoomLevel(): number { + const devicePixelRatio = window.devicePixelRatio || 1; + return devicePixelRatio; + } + + getZoomLevel$() { + return this.zoomLevel$.asObservable(); + } + + isZoomOutOfRange(minZoom: number = 0.95, maxZoom: number = 1.05): boolean { + const currentZoom = this.getZoomLevel(); + return currentZoom < minZoom || currentZoom > maxZoom; + } +} diff --git a/src/app/zoom-warning/zoom-warning.component.css b/src/app/zoom-warning/zoom-warning.component.css new file mode 100644 index 0000000..21fcb9c --- /dev/null +++ b/src/app/zoom-warning/zoom-warning.component.css @@ -0,0 +1,14 @@ +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(20px) scale(0.9); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.zoom-info-box { + animation: slideIn 0.3s ease-out; +} diff --git a/src/app/zoom-warning/zoom-warning.component.html b/src/app/zoom-warning/zoom-warning.component.html new file mode 100644 index 0000000..ea79125 --- /dev/null +++ b/src/app/zoom-warning/zoom-warning.component.html @@ -0,0 +1,81 @@ +@if (isOutOfRange && !isDismissed && !isMobile) { +
+ + +
+ + screenshot_monitor + + + warning + +
+ +

Browser-Zoom nicht optimal!

+

Ihr Browser-Zoom ist auf {{ currentZoomPercentage }}% eingestellt.
Für die beste Darstellung empfehlen wir 100%.

+ +
+
+

Browser-Zoom zurücksetzen:

+
+

+ Strg + / + + + + 0 + (Windows, Linux / Mac) +

+
+
+ +
+

Windows-Skalierung prüfen:

+
    +
  1. Öffnen Sie die Windows-Einstellungen
  2. +
  3. Navigieren Sie zu SystemBildschirm
  4. +
  5. Setzen Sie unter "Skalierung" den Wert auf 100%
  6. +
+
+
+
+} + +@if (showMobileWarning) { +
+
+ +

InfiniMotion

+
+
+
+
+
+ + screenshot_monitor + + + warning + +
+ +

Nur am PC verfügbar

+ +

+ Diese Anwendung ist für die Nutzung am Desktop-PC optimiert und kann auf mobilen Geräten nicht verwendet werden. +

+ +
+

Deine derzeitige URL:

+

+ {{ currentUrl }} +

+
+
+
+} diff --git a/src/app/zoom-warning/zoom-warning.component.ts b/src/app/zoom-warning/zoom-warning.component.ts new file mode 100644 index 0000000..8802167 --- /dev/null +++ b/src/app/zoom-warning/zoom-warning.component.ts @@ -0,0 +1,100 @@ +import { DeviceDetectionService } from './../device-detection.service'; +import { Component, HostListener, inject, OnDestroy, OnInit } from '@angular/core'; +import { filter, Subject, takeUntil } from 'rxjs'; +import { ZoomDetectionService } from '../zoom-detection.service'; +import { NavigationEnd, Router } from '@angular/router'; + +@Component({ + selector: 'app-zoom-warning', + standalone: false, + templateUrl: './zoom-warning.component.html', + styleUrl: './zoom-warning.component.css', +}) +export class ZoomWarningComponent implements OnInit, OnDestroy { + currentZoomPercentage = 100; + isOutOfRange = false; + isDismissed = false; + isMobile = false; + showMobileWarning = false; + currentUrl = ''; + + private destroy$ = new Subject(); + private currentZoomLevel = 1; + private lastZoomLevel = 0; + + zoomDetectionService = inject(ZoomDetectionService); + deviceDetectionService = inject(DeviceDetectionService) + + constructor(private router: Router) { + this.isMobile = this.deviceDetectionService.isMobile(); + this.currentUrl = window.location.href; + this.checkIfShouldShowMobileWarning(); + } + + ngOnInit() { + this.router.events + .pipe( + filter(event => event instanceof NavigationEnd), + takeUntil(this.destroy$) + ) + .subscribe(() => { + this.checkIfShouldShowMobileWarning(); + }); + + if (!this.isMobile) { + this.updateZoomInfo(); + + this.zoomDetectionService.getZoomLevel$() + .pipe(takeUntil(this.destroy$)) + .subscribe((newZoomLevel) => { + if (Math.abs(newZoomLevel - this.lastZoomLevel) > 0.01) { + this.lastZoomLevel = newZoomLevel; + this.isDismissed = false; + this.updateZoomInfo(); + } + }); + } + } + + private checkIfShouldShowMobileWarning() { + if (!this.isMobile) { + this.showMobileWarning = false; + return; + } + + const currentRoute = this.router.routerState.root; + const allowMobile = this.getRouteData(currentRoute, 'allowMobile'); + + this.showMobileWarning = !allowMobile; + } + + private getRouteData(route: any, key: string): any { + while (route) { + if (route.snapshot?.data?.[key] !== undefined) { + return route.snapshot.data[key]; + } + route = route.firstChild; + } + return false; + } + + private updateZoomInfo() { + this.currentZoomLevel = this.zoomDetectionService.getZoomLevel(); + this.currentZoomPercentage = Math.round(this.currentZoomLevel * 100); + this.isOutOfRange = this.zoomDetectionService.isZoomOutOfRange(); + } + + getCompensationTransform(): string { + const scale = 1 / this.currentZoomLevel; + return `scale(${scale})`; + } + + dismissWarning() { + this.isDismissed = true; + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +}