From 003d3b0187ff6058770ee3d1d46b9098e615dccc Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 29 Dec 2025 00:58:10 +0100 Subject: [PATCH] feat: Add waypoint modal and preserve offline user locations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add WaypointModal component with delete and navigate actions - Navigate button opens Google Maps directions to waypoint - Delete button removes waypoint with confirmation - Sync server now preserves offline users' last locations - Users marked as offline instead of removed when disconnecting - Stale participant cleanup still runs after 1 hour threshold 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/app/[slug]/page.tsx | 27 +++++++- src/components/room/WaypointModal.tsx | 93 +++++++++++++++++++++++++++ sync-server/server.js | 13 ++-- 3 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 src/components/room/WaypointModal.tsx diff --git a/src/app/[slug]/page.tsx b/src/app/[slug]/page.tsx index 394c8ca..ba3ec22 100644 --- a/src/app/[slug]/page.tsx +++ b/src/app/[slug]/page.tsx @@ -9,7 +9,8 @@ import ParticipantList from '@/components/room/ParticipantList'; import RoomHeader from '@/components/room/RoomHeader'; import ShareModal from '@/components/room/ShareModal'; import MeetingPointModal from '@/components/room/MeetingPointModal'; -import type { Participant, ParticipantLocation } from '@/types'; +import WaypointModal from '@/components/room/WaypointModal'; +import type { Participant, ParticipantLocation, Waypoint } from '@/types'; // Dynamic import for map to avoid SSR issues with MapLibre const DualMapView = dynamic(() => import('@/components/map/DualMapView'), { @@ -31,6 +32,7 @@ export default function RoomPage() { const [showMeetingPoint, setShowMeetingPoint] = useState(false); const [currentUser, setCurrentUser] = useState<{ name: string; emoji: string } | null>(null); const [selectedParticipant, setSelectedParticipant] = useState(null); + const [selectedWaypoint, setSelectedWaypoint] = useState(null); const [shouldAutoStartSharing, setShouldAutoStartSharing] = useState(false); // Load user and sharing preference from localStorage @@ -274,7 +276,7 @@ export default function RoomPage() { setShowParticipants(true); }} onWaypointClick={(w) => { - console.log('Waypoint clicked:', w.name); + setSelectedWaypoint(w); }} onIndoorPositionSet={updateIndoorPosition} /> @@ -316,6 +318,27 @@ export default function RoomPage() { }} /> )} + + {/* Waypoint Detail Modal */} + {selectedWaypoint && ( + setSelectedWaypoint(null)} + onDelete={() => { + removeWaypoint(selectedWaypoint.id); + setSelectedWaypoint(null); + }} + onNavigate={() => { + const { latitude, longitude } = selectedWaypoint.location; + window.open( + `https://www.google.com/maps/dir/?api=1&destination=${latitude},${longitude}`, + '_blank' + ); + setSelectedWaypoint(null); + }} + /> + )} ); } diff --git a/src/components/room/WaypointModal.tsx b/src/components/room/WaypointModal.tsx new file mode 100644 index 0000000..72f9477 --- /dev/null +++ b/src/components/room/WaypointModal.tsx @@ -0,0 +1,93 @@ +'use client'; + +import type { Waypoint } from '@/types'; + +interface WaypointModalProps { + waypoint: Waypoint; + canDelete: boolean; + onClose: () => void; + onDelete: () => void; + onNavigate: () => void; +} + +export default function WaypointModal({ + waypoint, + canDelete, + onClose, + onDelete, + onNavigate, +}: WaypointModalProps) { + const handleDelete = () => { + if (confirm(`Delete "${waypoint.name}"?`)) { + onDelete(); + } + }; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Header */} +
+
+ {waypoint.emoji || '📍'} +
+
+

{waypoint.name}

+

+ {waypoint.type === 'meeting_point' ? 'Meeting Point' : 'Waypoint'} +

+
+ +
+ + {/* Location info */} +
+ {waypoint.location.indoor ? ( + Level {waypoint.location.indoor.level} + ) : ( + + {waypoint.location.latitude.toFixed(5)}, {waypoint.location.longitude.toFixed(5)} + + )} +
+ + {/* Actions */} +
+ + + {canDelete && ( + + )} +
+
+
+ ); +} diff --git a/sync-server/server.js b/sync-server/server.js index e779a55..dcca430 100644 --- a/sync-server/server.js +++ b/sync-server/server.js @@ -157,13 +157,16 @@ function handleClose(ws) { const clientInfo = clients.get(ws); if (clientInfo) { const room = rooms.get(clientInfo.roomSlug); - if (room && clientInfo.participantId) { - delete room.participants[clientInfo.participantId]; + if (room && clientInfo.participantId && room.participants[clientInfo.participantId]) { + // Don't delete - mark as offline and preserve last location + room.participants[clientInfo.participantId].status = 'offline'; + room.participants[clientInfo.participantId].lastSeen = Date.now(); broadcast(clientInfo.roomSlug, { - type: 'leave', - participantId: clientInfo.participantId + type: 'status', + participantId: clientInfo.participantId, + status: 'offline' }); - console.log(`[${clientInfo.roomSlug}] Connection closed: ${clientInfo.participantId}`); + console.log(`[${clientInfo.roomSlug}] User went offline: ${clientInfo.participantId} (location preserved)`); } clients.delete(ws); }