Skip to content

Commit fc819b5

Browse files
committed
fix: update summoning states
1 parent b98f50b commit fc819b5

6 files changed

Lines changed: 1246 additions & 75 deletions

File tree

frontend/components/map-view.tsx

Lines changed: 101 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
"use client";
22

33
import { Button } from "@/components/ui/button";
4-
import { Locate, Plus, Minus, ArrowUpCircle } from "lucide-react";
4+
import {
5+
Locate,
6+
Plus,
7+
Minus,
8+
ArrowUpCircle,
9+
EyeOff,
10+
ImageIcon,
11+
SatelliteIcon,
12+
MapIcon,
13+
} from "lucide-react";
514
import mapboxgl, { LngLatLike } from "mapbox-gl";
615
import React, { useRef, useEffect, useState } from "react";
716
import "mapbox-gl/dist/mapbox-gl.css";
@@ -10,6 +19,7 @@ import MapboxDraw from "@mapbox/mapbox-gl-draw";
1019
import { inspect } from "@/api/inspect";
1120
import { toast } from "sonner";
1221
import { summon } from "@/api/summon";
22+
import { SummonDialog } from "@/components/summon-dialog";
1323

1424
const INITIAL_CENTER: { lng: number; lat: number } = {
1525
lng: -88.23556018270287,
@@ -27,29 +37,37 @@ export function MapView() {
2737
lat: number;
2838
} | null>(null);
2939
const [center, setCenter] = useState<{ lng: number; lat: number }>(
30-
INITIAL_CENTER
40+
INITIAL_CENTER,
3141
);
3242
const [boundingBox, setBoundingBox] = useState<LngLatLike[]>([]);
3343
const [zoom, setZoom] = useState(INITIAL_ZOOM);
3444
const [pitch, _] = useState(INITIAL_PITCH);
35-
45+
const [satelliteMode, setSatelliteMode] = useState(false);
3646
const [isSummoning, setIsSummoning] = useState(false);
3747

48+
const streetStyle = "mapbox://styles/mapbox/standard";
49+
const satelliteStyle = "mapbox://styles/mapbox/satellite-v9";
3850
const mapboxToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN;
3951

4052
const handleSummon = async () => {
41-
toast.info(`Summoning to ${userLocation?.lat} ${userLocation?.lng}`)
53+
toast.info(
54+
`Summon to ${userLocation?.lat} ${userLocation?.lng} was placed into queue.`,
55+
{
56+
description:
57+
"Summoning will begin shortly (as soon as GEM will pick up the event)",
58+
},
59+
);
4260
setIsSummoning(true);
4361
const coords = userLocation ? userLocation : center;
4462

45-
const summonReq = await summon(coords.lng, coords.lat);
63+
const summonReq = await summon(coords.lng, coords.lat);
4664

47-
if (!summonReq.ok) {
48-
const errorData = await summonReq.json();
49-
console.error("Summon error:", errorData.detail || errorData);
50-
setIsSummoning(false);
51-
return;
52-
}
65+
if (!summonReq.ok) {
66+
const errorData = await summonReq.json();
67+
console.error("Summon error:", errorData.detail || errorData);
68+
setIsSummoning(false);
69+
return;
70+
}
5371
};
5472

5573
const handleZoomIn = () => {
@@ -59,6 +77,13 @@ export function MapView() {
5977
mapRef.current?.zoomOut();
6078
};
6179

80+
const toggleStyle = () => {
81+
if (!mapRef.current) return;
82+
const newMode = !satelliteMode;
83+
setSatelliteMode(newMode);
84+
mapRef.current.setStyle(newMode ? satelliteStyle : streetStyle);
85+
};
86+
6287
useEffect(() => {
6388
if (mapboxToken) {
6489
mapboxgl.accessToken = mapboxToken;
@@ -73,8 +98,8 @@ export function MapView() {
7398
// style: "mapbox://styles/mapbox/satellite-v9",
7499
maxBounds: [
75100
[-88.2368, 40.0925], // Southwest coordinates
76-
[-88.2346, 40.0935] // Northeast coordinates
77-
]
101+
[-88.2346, 40.0935], // Northeast coordinates
102+
],
78103
});
79104

80105
const draw = new MapboxDraw({
@@ -131,17 +156,17 @@ export function MapView() {
131156
}
132157

133158
const marker = new mapboxgl.Marker({
134-
draggable: true
159+
draggable: true,
135160
})
136-
.setLngLat(INITIAL_CENTER)
137-
.addTo(mapRef.current);
161+
.setLngLat(INITIAL_CENTER)
162+
.addTo(mapRef.current);
138163

139164
function onDragEnd() {
140165
const lngLat = marker.getLngLat();
141-
setUserLocation({lat: lngLat.lat, lng: lngLat.lng})
166+
setUserLocation({ lat: lngLat.lat, lng: lngLat.lng });
142167
}
143168

144-
marker.on('dragend', onDragEnd);
169+
marker.on("dragend", onDragEnd);
145170

146171
mapRef.current.on("move", () => {
147172
// get the current center coordinates and zoom level from the map
@@ -166,58 +191,73 @@ export function MapView() {
166191

167192
return (
168193
<div className="relative w-full h-full bg-neutral-800 flex items-center justify-center">
194+
<SummonDialog />
169195
<div id="map-container" ref={mapContainerRef} />
170196
<div className="absolute top-6 left-6 bg-neutral-900/50 p-2 rounded-sm md:text-base text-xs md:max-w-none max-w-[200px]">
171197
Longitude: {center.lng.toFixed(4)} | Latitude: {center.lat.toFixed(4)} |
172198
Zoom: {zoom.toFixed(2)}
173199
</div>
174200

175201
{/* Map controls */}
176-
<div className="absolute bottom-6 right-6 flex flex-col gap-2">
177-
<Button
178-
variant="secondary"
179-
size="icon"
180-
className="rounded-full bg-neutral-900 hover:bg-neutral-800"
181-
onClick={handleZoomIn}
182-
>
183-
<Plus
184-
className="h-5 w-5"
185-
onClick={() => setZoom(Math.min(zoom + 1, 20))}
186-
/>
187-
</Button>
188-
<Button
189-
variant="secondary"
190-
size="icon"
191-
className="rounded-full bg-neutral-900 hover:bg-neutral-800"
192-
onClick={handleZoomOut}
193-
>
194-
<Minus
195-
className="h-5 w-5"
196-
onClick={() => setZoom(Math.max(zoom - 1, 10))}
197-
/>
198-
</Button>
199-
</div>
200-
201-
<div className="absolute bottom-6 left-6 flex gap-2">
202-
<Button
203-
variant="secondary"
204-
className="rounded-
202+
<div className="absolute bottom-6 flex sm:flex-row flex-col-reverse gap-4 justify-between w-full px-6 sm:items-end">
203+
<div className="flex flex-col sm:flex-row gap-2">
204+
<Button
205+
variant="secondary"
206+
className="rounded-
205207
full bg-neutral-900 hover:bg-neutral-800"
206-
onClick={handleSummon}
207-
disabled={isSummoning || !userLocation}
208-
>
209-
<Locate className="h-5 w-5 mr-2" />
210-
{isSummoning ? "Summoning..." : "Summon My Car"}
211-
</Button>
212-
<Button
213-
variant="secondary"
214-
className="rounded-
208+
onClick={handleSummon}
209+
disabled={!userLocation}
210+
>
211+
<Locate className="h-5 w-5 mr-2" />
212+
Summon My Car
213+
</Button>
214+
<Button
215+
variant="secondary"
216+
className="rounded-
215217
full bg-neutral-900 hover:bg-neutral-800"
216-
onClick={() => inspect(boundingBox)}
217-
>
218-
<ArrowUpCircle className="h-5 w-5 mr-2" />
219-
<span>Inspect Region</span>
220-
</Button>
218+
onClick={() => inspect(boundingBox)}
219+
>
220+
<ArrowUpCircle className="h-5 w-5 mr-2" />
221+
<span>Inspect Region</span>
222+
</Button>
223+
</div>
224+
225+
<div className="flex sm:flex-col gap-2">
226+
<Button
227+
variant="secondary"
228+
size="icon"
229+
className="rounded-full bg-neutral-900 hover:bg-neutral-800"
230+
onClick={toggleStyle}
231+
>
232+
{satelliteMode ? (
233+
<SatelliteIcon className="h-5 w-5" />
234+
) : (
235+
<MapIcon className="h-5 w-5" />
236+
)}
237+
</Button>
238+
<Button
239+
variant="secondary"
240+
size="icon"
241+
className="rounded-full bg-neutral-900 hover:bg-neutral-800"
242+
onClick={handleZoomIn}
243+
>
244+
<Plus
245+
className="h-5 w-5"
246+
onClick={() => setZoom(Math.min(zoom + 1, 20))}
247+
/>
248+
</Button>
249+
<Button
250+
variant="secondary"
251+
size="icon"
252+
className="rounded-full bg-neutral-900 hover:bg-neutral-800"
253+
onClick={handleZoomOut}
254+
>
255+
<Minus
256+
className="h-5 w-5"
257+
onClick={() => setZoom(Math.max(zoom - 1, 10))}
258+
/>
259+
</Button>
260+
</div>
221261
</div>
222262
</div>
223263
);
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"use client"
2+
3+
import { useState, useEffect, useRef } from "react"
4+
import { motion, AnimatePresence } from "framer-motion"
5+
import {Dialog, DialogContent, DialogTitle} from "@/components/ui/dialog"
6+
import { Button } from "@/components/ui/button"
7+
import { Car, CheckCircle, Loader2 } from "lucide-react"
8+
import { Progress } from "@/components/ui/progress"
9+
import { getCarStatus, PlannerEnum } from "@/api/status"
10+
11+
export const SummonDialog = () => {
12+
const [carStatus, setCarStatus] = useState<PlannerEnum | null>(null)
13+
const [showDialog, setShowDialog] = useState(false)
14+
const [progress, setProgress] = useState(0)
15+
const [showSuccess, setShowSuccess] = useState(false)
16+
17+
const prevStatus = useRef<PlannerEnum | null>(null)
18+
19+
useEffect(() => {
20+
let statusInterval: number
21+
let progressInterval: number
22+
23+
const fetchStatus = async () => {
24+
const status = await getCarStatus()
25+
26+
const prev = prevStatus.current
27+
// detect transition non-idle -> idle
28+
if (prev && prev !== PlannerEnum.IDLE && status === PlannerEnum.IDLE) {
29+
setShowSuccess(true)
30+
setTimeout(() => {
31+
setShowDialog(false)
32+
setShowSuccess(false)
33+
}, 5000)
34+
}
35+
// detect idle -> non-idle
36+
if ((prev === PlannerEnum.IDLE || prev === null) && status && status !== PlannerEnum.IDLE) {
37+
setShowDialog(true)
38+
setProgress(0)
39+
}
40+
prevStatus.current = status
41+
setCarStatus(status)
42+
}
43+
44+
const updateProgress = () => {
45+
if (carStatus === PlannerEnum.RRT_STAR || carStatus === PlannerEnum.HYBRID_A_STAR) {
46+
setProgress(prev => Math.min(prev + 2, 100))
47+
} else if (carStatus === PlannerEnum.SUMMON_DRIVING || carStatus === PlannerEnum.LEAVE_PARKING) {
48+
setProgress(prev => Math.min(prev + 0.5, 95))
49+
} else if (carStatus === PlannerEnum.PARKING || carStatus === PlannerEnum.PARALLEL_PARKING) {
50+
setProgress(prev => Math.min(prev + 1, 98))
51+
} else if (carStatus === PlannerEnum.IDLE) {
52+
setProgress(100)
53+
} else {
54+
setProgress(0)
55+
}
56+
}
57+
58+
fetchStatus()
59+
statusInterval = window.setInterval(fetchStatus, 3000)
60+
progressInterval = window.setInterval(updateProgress, 200)
61+
62+
return () => {
63+
window.clearInterval(statusInterval)
64+
window.clearInterval(progressInterval)
65+
}
66+
}, [carStatus])
67+
68+
const getStatusMessage = () => {
69+
switch (carStatus) {
70+
case PlannerEnum.RRT_STAR: return "Planning optimal route..."
71+
case PlannerEnum.HYBRID_A_STAR: return "Calculating path..."
72+
case PlannerEnum.PARKING: return "Parking your car"
73+
case PlannerEnum.LEAVE_PARKING: return "Exiting parking space"
74+
case PlannerEnum.SUMMON_DRIVING: return "Your car is on the way"
75+
case PlannerEnum.PARALLEL_PARKING: return "Parallel parking in progress"
76+
case PlannerEnum.IDLE: return "Ready"
77+
default: return "Connecting to your car..."
78+
}
79+
}
80+
81+
const getStatusIcon = () => {
82+
if (carStatus === PlannerEnum.RRT_STAR || carStatus === PlannerEnum.HYBRID_A_STAR) {
83+
return <Loader2 className="h-10 w-10 text-blue-400 animate-spin" />
84+
} else if (carStatus === PlannerEnum.SUMMON_DRIVING || carStatus === PlannerEnum.LEAVE_PARKING) {
85+
return <Car className="h-10 w-10 text-blue-400" />
86+
} else if (carStatus === PlannerEnum.PARKING || carStatus === PlannerEnum.PARALLEL_PARKING) {
87+
return <Car className="h-10 w-10 text-yellow-400" />
88+
} else if (carStatus === PlannerEnum.IDLE) {
89+
return <CheckCircle className="h-10 w-10 text-green-400" />
90+
} else {
91+
return <Loader2 className="h-10 w-10 animate-spin" />
92+
}
93+
}
94+
95+
return (
96+
<Dialog
97+
open={showDialog}
98+
onOpenChange={(open) => {
99+
if (carStatus === PlannerEnum.IDLE || showSuccess) setShowDialog(open)
100+
}}
101+
>
102+
<DialogContent className="bg-neutral-900 border-neutral-800 p-0 overflow-hidden max-w-md w-full rounded-xl">
103+
<DialogTitle className="bg-neutral-800 text-white p-4 text-lg">Vehicle Status</DialogTitle>
104+
<div className="p-6">
105+
<AnimatePresence mode="wait">
106+
{showSuccess ? (
107+
<motion.div
108+
key="success"
109+
initial={{ opacity: 0, scale: 0.8 }}
110+
animate={{ opacity: 1, scale: 1 }}
111+
exit={{ opacity: 0, scale: 0.8 }}
112+
className="flex flex-col items-center justify-center text-center space-y-4"
113+
>
114+
<CheckCircle className="h-16 w-16 text-green-400" />
115+
<h2 className="text-2xl font-medium">Your car has arrived</h2>
116+
<p className="text-neutral-400">Your GEM is ready and waiting for you</p>
117+
<Button onClick={() => { setShowDialog(false); setShowSuccess(false) }} className="mt-4">Close</Button>
118+
</motion.div>
119+
) : (
120+
<motion.div
121+
key="status"
122+
initial={{ opacity: 0 }}
123+
animate={{ opacity: 1 }}
124+
exit={{ opacity: 0 }}
125+
className="flex flex-col items-center justify-center text-center space-y-6"
126+
>
127+
{getStatusIcon()}
128+
<div className="space-y-2 w-full">
129+
<h2 className="text-xl font-medium">{getStatusMessage()}</h2>
130+
{(carStatus && carStatus !== PlannerEnum.IDLE) && (
131+
<>
132+
<Progress value={progress} className="h-2 bg-neutral-800">
133+
<div
134+
className={`h-full ${carStatus === PlannerEnum.PARKING || carStatus === PlannerEnum.PARALLEL_PARKING ? "bg-yellow-500" : "bg-blue-500"}`}
135+
style={{ width: `${progress}%` }}
136+
/>
137+
</Progress>
138+
<p className="text-xs text-neutral-400 mt-2">
139+
{carStatus === PlannerEnum.RRT_STAR || carStatus === PlannerEnum.HYBRID_A_STAR
140+
? "Planning optimal route"
141+
: carStatus === PlannerEnum.PARKING || carStatus === PlannerEnum.PARALLEL_PARKING
142+
? "Finding the perfect spot"
143+
: "Estimated arrival time: soon"}
144+
</p>
145+
</>
146+
)}
147+
</div>
148+
{carStatus === PlannerEnum.IDLE && <Button onClick={() => setShowDialog(false)}>Close</Button>}
149+
</motion.div>
150+
)}
151+
</AnimatePresence>
152+
</div>
153+
</DialogContent>
154+
</Dialog>
155+
)
156+
}

0 commit comments

Comments
 (0)