feat: Fix navigate button and add PWA install prompt

- Navigate button now opens Google Maps directions to friend's location
- Added PWA install banner for non-installed users
- iOS users get manual instructions for Add to Home Screen
- Banner can be dismissed and stays dismissed for session

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-12-29 01:54:25 +01:00
parent d6a20f9500
commit 7410246a01
3 changed files with 185 additions and 4 deletions

View File

@ -11,6 +11,7 @@ import RoomHeader from '@/components/room/RoomHeader';
import ShareModal from '@/components/room/ShareModal';
import MeetingPointModal from '@/components/room/MeetingPointModal';
import WaypointModal from '@/components/room/WaypointModal';
import InstallBanner from '@/components/room/InstallBanner';
import type { Participant, ParticipantLocation, Waypoint } from '@/types';
// Dynamic import for map to avoid SSR issues with MapLibre
@ -242,11 +243,17 @@ export default function RoomPage() {
};
}, [leave]);
// Navigate to participant
// Navigate to participant - opens Google Maps navigation
const handleNavigateTo = (participant: Participant) => {
setSelectedParticipant(participant);
// TODO: Implement navigation route display
console.log('Navigate to:', participant.name);
if (!participant.location) {
console.log('No location for participant:', participant.name);
return;
}
const { latitude, longitude } = participant.location;
window.open(
`https://www.google.com/maps/dir/?api=1&destination=${latitude},${longitude}`,
'_blank'
);
};
// Loading state
@ -283,6 +290,9 @@ export default function RoomPage() {
return (
<div className="h-screen w-screen flex flex-col overflow-hidden bg-rmaps-dark">
{/* PWA Install Banner */}
<InstallBanner />
{/* Header */}
<RoomHeader
roomSlug={slug}

View File

@ -0,0 +1,93 @@
'use client';
import { useState } from 'react';
import { usePWAInstall } from '@/hooks/usePWAInstall';
export default function InstallBanner() {
const { isInstallable, isInstalled, isIOS, promptInstall } = usePWAInstall();
const [dismissed, setDismissed] = useState(false);
const [showIOSInstructions, setShowIOSInstructions] = useState(false);
// Don't show if already installed, dismissed, or not installable (and not iOS)
if (isInstalled || dismissed) return null;
if (!isInstallable && !isIOS) return null;
// Check if already dismissed this session
if (typeof window !== 'undefined') {
const dismissedSession = sessionStorage.getItem('rmaps_install_dismissed');
if (dismissedSession) return null;
}
const handleDismiss = () => {
setDismissed(true);
sessionStorage.setItem('rmaps_install_dismissed', 'true');
};
const handleInstall = async () => {
if (isIOS) {
setShowIOSInstructions(true);
} else {
const success = await promptInstall();
if (success) {
handleDismiss();
}
}
};
return (
<>
<div className="bg-rmaps-primary/20 border-b border-rmaps-primary/30 px-4 py-2 flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<span className="text-lg">📲</span>
<span className="text-white/90">Install rMaps for the best experience</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleInstall}
className="px-3 py-1 bg-rmaps-primary hover:bg-rmaps-primary/80 rounded-full text-white text-xs font-medium transition-colors"
>
Install
</button>
<button
onClick={handleDismiss}
className="p-1 hover:bg-white/10 rounded-full transition-colors"
>
<svg className="w-4 h-4 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* iOS Instructions Modal */}
{showIOSInstructions && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/60" onClick={() => setShowIOSInstructions(false)} />
<div className="relative bg-rmaps-card rounded-2xl p-6 max-w-sm w-full">
<h3 className="text-lg font-semibold text-white mb-4">Install on iOS</h3>
<ol className="text-white/70 text-sm space-y-3">
<li className="flex items-start gap-3">
<span className="text-lg">1.</span>
<span>Tap the <strong className="text-white">Share</strong> button in Safari</span>
</li>
<li className="flex items-start gap-3">
<span className="text-lg">2.</span>
<span>Scroll down and tap <strong className="text-white">"Add to Home Screen"</strong></span>
</li>
<li className="flex items-start gap-3">
<span className="text-lg">3.</span>
<span>Tap <strong className="text-white">"Add"</strong> in the top right</span>
</li>
</ol>
<button
onClick={() => setShowIOSInstructions(false)}
className="mt-6 w-full btn-primary"
>
Got it
</button>
</div>
</div>
)}
</>
);
}

View File

@ -0,0 +1,78 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
interface BeforeInstallPromptEvent extends Event {
prompt(): Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}
export function usePWAInstall() {
const [installPrompt, setInstallPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const [isInstallable, setIsInstallable] = useState(false);
const [isInstalled, setIsInstalled] = useState(false);
useEffect(() => {
// Check if already installed (standalone mode)
if (window.matchMedia('(display-mode: standalone)').matches) {
setIsInstalled(true);
return;
}
// Check iOS standalone
if ((navigator as any).standalone === true) {
setIsInstalled(true);
return;
}
const handleBeforeInstall = (e: Event) => {
e.preventDefault();
setInstallPrompt(e as BeforeInstallPromptEvent);
setIsInstallable(true);
};
const handleAppInstalled = () => {
setIsInstalled(true);
setIsInstallable(false);
setInstallPrompt(null);
};
window.addEventListener('beforeinstallprompt', handleBeforeInstall);
window.addEventListener('appinstalled', handleAppInstalled);
return () => {
window.removeEventListener('beforeinstallprompt', handleBeforeInstall);
window.removeEventListener('appinstalled', handleAppInstalled);
};
}, []);
const promptInstall = useCallback(async () => {
if (!installPrompt) return false;
try {
await installPrompt.prompt();
const result = await installPrompt.userChoice;
if (result.outcome === 'accepted') {
setIsInstalled(true);
setIsInstallable(false);
setInstallPrompt(null);
return true;
}
} catch (error) {
console.error('Install prompt error:', error);
}
return false;
}, [installPrompt]);
const isIOS = typeof navigator !== 'undefined' &&
/iPad|iPhone|iPod/.test(navigator.userAgent) &&
!(window as any).MSStream;
return {
isInstallable,
isInstalled,
isIOS,
promptInstall,
};
}