Add zoom and device detection with warning component

Introduces DeviceDetectionService and ZoomDetectionService to detect mobile devices and browser zoom level. Adds ZoomWarningComponent to display warnings for unsupported mobile devices and non-optimal browser zoom, and integrates it into the app layout. Updates routing to allow mobile access for the 'poc-model' route.
This commit is contained in:
2025-11-18 23:00:18 +01:00
parent 78144d7447
commit f4eb700ab4
8 changed files with 261 additions and 1 deletions

View File

@@ -64,6 +64,7 @@ import { PurchaseSuccessComponent } from './purchase-success/purchase-success.co
import { PurchaseFailedComponent } from './purchase-failed/purchase-failed.component'; import { PurchaseFailedComponent } from './purchase-failed/purchase-failed.component';
import { TicketSmallComponent } from './ticket-small/ticket-small.component'; import { TicketSmallComponent } from './ticket-small/ticket-small.component';
import { TicketListComponent } from './ticket-list/ticket-list.component'; import { TicketListComponent } from './ticket-list/ticket-list.component';
import { ZoomWarningComponent } from './zoom-warning/zoom-warning.component';
@NgModule({ @NgModule({
@@ -107,6 +108,7 @@ import { TicketListComponent } from './ticket-list/ticket-list.component';
PurchaseFailedComponent, PurchaseFailedComponent,
TicketSmallComponent, TicketSmallComponent,
TicketListComponent, TicketListComponent,
ZoomWarningComponent,
], ],
imports: [ imports: [
AppRoutingModule, AppRoutingModule,

View File

@@ -12,7 +12,7 @@ import { AuthGuard } from './auth.guard';
const routes: Routes = [ const routes: Routes = [
// Seiten ohne Layout // Seiten ohne Layout
{ path: 'landing', component: HomeComponent }, { path: 'landing', component: HomeComponent },
{ path: 'poc-model', component: PocModelComponent }, { path: 'poc-model', component: PocModelComponent, data: { allowMobile: true } },
// Seiten mit MainLayout // Seiten mit MainLayout
{ {

View File

@@ -1 +1,2 @@
<app-zoom-warning></app-zoom-warning>
<router-outlet /> <router-outlet />

View File

@@ -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();
}
}

View File

@@ -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<number>(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;
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,81 @@
@if (isOutOfRange && !isDismissed && !isMobile) {
<div class="zoom-info-box fixed bottom-5 right-5 z-9999 w-[500px] bg-amber-300 border-4 border-dashed rounded-md shadow-lg p-6 py-8 items-center justify-center flex flex-col space-y-2 origin-bottom-right transition-all duration-300" [style.transform]="getCompensationTransform()">
<button
(click)="dismissWarning()"
class="absolute top-2 right-2 w-10 h-10 flex items-center justify-center rounded-full hover:bg-amber-400 transition-colors"
aria-label="Warnung schließen">
<span class="text-3xl leading-none">×</span>
</button>
<div class="relative mb-5">
<mat-icon class="material-symbols-outlined" style="font-size: 100px; width: 100px; height: 100px">
screenshot_monitor
</mat-icon>
<mat-icon class="material-symbols-outlined absolute top-[calc(50%-9px)] left-1/2 -translate-x-1/2 -translate-y-1/2" style="font-size: 30px; width: 30px; height: 30px">
warning
</mat-icon>
</div>
<h1 class="text-xl font-bold">Browser-Zoom nicht optimal!</h1>
<p class="text-center">Ihr Browser-Zoom ist auf <strong>{{ currentZoomPercentage }}%</strong> eingestellt.<br>Für die beste Darstellung empfehlen wir <strong>100%</strong>.</p>
<div class="mt-4 text-sm space-y-3 w-full">
<div class="bg-white/50 rounded p-3">
<p class="font-semibold mb-2">Browser-Zoom zurücksetzen:</p>
<div class="space-y-1 ml-1">
<p class="flex items-center gap-1.5">
<kbd class="px-2 py-1 bg-white rounded shadow border border-gray-500 font-mono text-xs">Strg</kbd>
<span>/</span>
<kbd class="px-2 py-1 bg-white rounded shadow border border-gray-500 font-mono text-xs"></kbd>
<span>+</span>
<kbd class="px-2 py-1 bg-white rounded shadow border border-gray-500 font-mono text-xs">0</kbd>
<span class="px-1">(Windows, Linux / Mac)</span>
</p>
</div>
</div>
<div class="bg-white/50 rounded p-3">
<p class="font-semibold mb-2">Windows-Skalierung prüfen:</p>
<ol class="list-decimal ml-6 space-y-1">
<li>Öffnen Sie die <strong>Windows-Einstellungen</strong></li>
<li>Navigieren Sie zu <strong>System</strong><strong>Bildschirm</strong></li>
<li>Setzen Sie unter <strong>"Skalierung"</strong> den Wert auf <strong>100%</strong></li>
</ol>
</div>
</div>
</div>
}
@if (showMobileWarning) {
<div class="header z-99999 px-8 pt-4 pb-3 relative bg-white text-center">
<div class="flex items-center justify-center space-x-4 transition m-auto">
<img src="assets/logo.png" class="h-10 w-10 transform scale-175 translate-y-px" />
<h1 class="text-3xl font-semibold tracking-wide">InfiniMotion</h1>
</div>
</div>
<div class="fixed inset-0 z-99998 bg-amber-300 flex items-center justify-center p-6">
<div class="max-w-md w-full text-center space-y-6 mt-10">
<div class="relative inline-block">
<mat-icon class="material-symbols-outlined" style="font-size: 120px; width: 120px; height: 120px">
screenshot_monitor
</mat-icon>
<mat-icon class="material-symbols-outlined absolute top-[calc(50%-11px)] left-1/2 -translate-x-1/2 -translate-y-1/2" style="font-size: 35px; width: 35px; height: 35px">
warning
</mat-icon>
</div>
<h1 class="text-2xl font-bold text-gray-800">Nur am PC verfügbar</h1>
<p class="text-base text-gray-700">
Diese Anwendung ist für die Nutzung am <strong>Desktop-PC</strong> optimiert und kann auf mobilen Geräten nicht verwendet werden.
</p>
<div class="text-xs text-gray-600 mt-12">
<p>Deine derzeitige URL:</p>
<p class="font-mono bg-white/70 px-3 py-2 rounded mt-1 break-all">
{{ currentUrl }}
</p>
</div>
</div>
</div>
}

View File

@@ -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<void>();
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();
}
}