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:
parent
d6a20f9500
commit
7410246a01
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue