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:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
<app-zoom-warning></app-zoom-warning>
|
||||||
<router-outlet />
|
<router-outlet />
|
||||||
|
|||||||
29
src/app/device-detection.service.ts
Normal file
29
src/app/device-detection.service.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/app/zoom-detection.service.ts
Normal file
33
src/app/zoom-detection.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/app/zoom-warning/zoom-warning.component.css
Normal file
14
src/app/zoom-warning/zoom-warning.component.css
Normal 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;
|
||||||
|
}
|
||||||
81
src/app/zoom-warning/zoom-warning.component.html
Normal file
81
src/app/zoom-warning/zoom-warning.component.html
Normal 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>
|
||||||
|
}
|
||||||
100
src/app/zoom-warning/zoom-warning.component.ts
Normal file
100
src/app/zoom-warning/zoom-warning.component.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user