Files
hoerdle/components/InstallPrompt.tsx
2025-11-28 15:36:06 +01:00

135 lines
4.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect } from 'react';
import { useTranslations } from 'next-intl';
export default function InstallPrompt() {
const t = useTranslations('InstallPrompt');
const [isIOS, setIsIOS] = useState(false);
const [isStandalone, setIsStandalone] = useState(false);
const [showPrompt, setShowPrompt] = useState(false);
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
useEffect(() => {
// Check if already in standalone mode
const isStandaloneMode = window.matchMedia('(display-mode: standalone)').matches || (window.navigator as any).standalone;
setIsStandalone(isStandaloneMode);
// Check if iOS
const userAgent = window.navigator.userAgent.toLowerCase();
const isIosDevice = /iphone|ipad|ipod/.test(userAgent);
setIsIOS(isIosDevice);
// Check if already dismissed
const isDismissed = localStorage.getItem('installPromptDismissed');
if (!isStandaloneMode && !isDismissed) {
if (isIosDevice) {
// Show prompt for iOS immediately if not dismissed
setShowPrompt(true);
} else {
// For Android/Desktop, wait for beforeinstallprompt
const handleBeforeInstallPrompt = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e);
setShowPrompt(true);
};
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
return () => {
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
};
}
}
}, []);
const handleInstallClick = async () => {
if (deferredPrompt) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
setDeferredPrompt(null);
setShowPrompt(false);
}
}
};
const handleDismiss = () => {
setShowPrompt(false);
localStorage.setItem('installPromptDismissed', 'true');
};
if (!showPrompt) return null;
return (
<div style={{
position: 'fixed',
bottom: '20px',
left: '20px',
right: '20px',
background: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(10px)',
padding: '1rem',
borderRadius: '1rem',
boxShadow: '0 10px 25px rgba(0,0,0,0.2)',
zIndex: 1000,
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
border: '1px solid rgba(0,0,0,0.1)',
animation: 'slideUp 0.5s ease-out'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
<div>
<h3 style={{ fontWeight: 'bold', fontSize: '1rem', marginBottom: '0.25rem' }}>{t('installApp')}</h3>
<p style={{ fontSize: '0.875rem', color: '#666' }}>
{t('installDescription')}
</p>
</div>
<button
onClick={handleDismiss}
style={{
background: 'none',
border: 'none',
fontSize: '1.25rem',
cursor: 'pointer',
padding: '0.25rem',
color: '#999'
}}
>
×
</button>
</div>
{isIOS ? (
<div style={{ fontSize: '0.875rem', background: '#f3f4f6', padding: '0.75rem', borderRadius: '0.5rem', marginTop: '0.5rem' }}>
{t('iosInstructions')} <span style={{ fontSize: '1.2rem' }}>{t('iosShare')}</span> {t('iosThen')} <span style={{ fontSize: '1.2rem' }}>+</span>
</div>
) : (
<button
onClick={handleInstallClick}
style={{
background: '#4f46e5',
color: 'white',
border: 'none',
padding: '0.75rem',
borderRadius: '0.5rem',
fontWeight: 'bold',
cursor: 'pointer',
marginTop: '0.5rem'
}}
>
{t('installButton')}
</button>
)}
<style jsx>{`
@keyframes slideUp {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
`}</style>
</div>
);
}