Files
rosterchirp-dev/frontend/src/main.jsx
2026-03-28 10:00:52 -04:00

109 lines
4.2 KiB
JavaScript

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import './index.css';
// Register service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('[SW] Registered, scope:', reg.scope))
.catch(err => console.error('[SW] Registration failed:', err));
});
}
// ─── Touch gesture handler ───────────────────────────────────────────────────
// Handles two behaviours in one unified listener set to avoid conflicts:
//
// 1. PINCH → font scale only (not viewport zoom).
// viewport has user-scalable=no so the browser never zooms the layout.
// We intercept the pinch and adjust --font-scale on <html> instead,
// which scales only text (rem-based font sizes). Persisted to localStorage.
// On first launch, html { font-size: 100% } inherits the Android system
// font size as the 1rem baseline automatically.
//
// 2. PULL-TO-REFRESH → blocked in PWA standalone mode only.
(function () {
const LS_KEY = 'rosterchirp_font_scale';
const MIN_SCALE = 0.8;
const MAX_SCALE = 2.0;
const isStandalone = window.matchMedia('(display-mode: standalone)').matches
|| window.navigator.standalone === true;
// Restore saved font scale on launch
const saved = parseFloat(localStorage.getItem(LS_KEY));
let currentScale = (saved >= MIN_SCALE && saved <= MAX_SCALE) ? saved : 1.0;
document.documentElement.style.setProperty('--font-scale', currentScale);
let pinchStartDist = null;
let pinchStartScale = currentScale;
let singleStartY = 0;
function getTouchDist(e) {
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
return Math.sqrt(dx * dx + dy * dy);
}
document.addEventListener('touchstart', function (e) {
if (e.touches.length === 2) {
pinchStartDist = getTouchDist(e);
pinchStartScale = currentScale;
} else if (e.touches.length === 1) {
singleStartY = e.touches[0].clientY;
}
}, { passive: true });
document.addEventListener('touchmove', function (e) {
if (e.touches.length === 2 && pinchStartDist !== null) {
// Two-finger pinch: scale fonts, not viewport.
// Skip when a lightbox is open — let the browser handle pinch natively there.
if (document.documentElement.dataset.lightboxOpen) return;
e.preventDefault();
const ratio = getTouchDist(e) / pinchStartDist;
const newScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, pinchStartScale * ratio));
currentScale = Math.round(newScale * 100) / 100;
document.documentElement.style.setProperty('--font-scale', currentScale);
} else if (e.touches.length === 1 && isStandalone) {
// Single finger: block pull-to-refresh only when no scrollable ancestor
// has scrolled content above the viewport.
// Without this ancestor check, document.scrollTop is always 0 in this
// flex layout, so the naive condition blocked ALL upward swipes (dy > 0),
// making any scroll container impossible to scroll back up after reaching
// the bottom — freezing the window.
const dy = e.touches[0].clientY - singleStartY;
if (dy > 0) {
let el = e.target;
let canScrollUp = false;
while (el && el !== document.documentElement) {
if (el.scrollTop > 0) { canScrollUp = true; break; }
el = el.parentElement;
}
if (!canScrollUp) e.preventDefault();
}
}
}, { passive: false });
document.addEventListener('touchend', function (e) {
if (e.touches.length < 2 && pinchStartDist !== null) {
pinchStartDist = null;
localStorage.setItem(LS_KEY, currentScale);
}
}, { passive: true });
})();
// Clear badge count when user focuses the app
window.addEventListener('focus', () => {
if (navigator.clearAppBadge) navigator.clearAppBadge().catch(() => {});
navigator.serviceWorker?.controller?.postMessage({ type: 'CLEAR_BADGE' });
});
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);