Skip to content

Commit 5f25d00

Browse files
authored
Merge pull request #12 from fcsonline/feat/ux-audit-improvements
feat: UX improvements from first-time user audit
2 parents 2aa1232 + 9b9c0c4 commit 5f25d00

17 files changed

Lines changed: 187 additions & 97 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ data/
88
tsconfig.tsbuildinfo
99
*.tsbuildinfo
1010
*.kmz
11+
audit-*.png
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
## Summary
2+
3+
UX improvements based on first-time user audit — better defaults, mobile fixes, and new map features.
4+
5+
## Changes
6+
7+
- Default waypoint altitude changed from 50m to 30m
8+
- Default drone model changed from DJI M30 to DJI Mavic 3E
9+
- Export KMZ button now shows a tooltip explaining why it's disabled
10+
- Removed redundant "Add 2+ waypoints" text from the sidebar footer
11+
- Fixed sidebar height on mobile using `h-dvh` so the sign-in button is always reachable
12+
- Removed EGM96 (MSL) height reference option
13+
- Elevation chart now shows a hint to drag waypoints for altitude adjustment
14+
- Added satellite imagery layer (Esri) as a base layer option
15+
- Changed guest sign-in prompt to "Guest mode. Sign in to save missions"

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"react": "^19.2.5",
2828
"react-dom": "^19.2.5",
2929
"react-leaflet": "^5.0.0",
30+
"sonner": "^2.0.7",
3031
"tailwind-merge": "^3.0.0",
3132
"zustand": "^5.0.3"
3233
},

packages/frontend/src/App.tsx

Lines changed: 79 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useState, useRef, useEffect, useMemo } from "react";
2+
import { toast } from "sonner";
23
import {
34
Download,
45
Upload,
@@ -175,7 +176,7 @@ export default function App() {
175176

176177
const handleExport = async () => {
177178
if (waypoints.length < 2) {
178-
alert("Need at least 2 waypoints to export");
179+
toast.warning("Need at least 2 waypoints to export");
179180
return;
180181
}
181182

@@ -195,7 +196,7 @@ export default function App() {
195196
a.click();
196197
URL.revokeObjectURL(url);
197198
} catch (err: any) {
198-
alert(`Export failed: ${err.message}`);
199+
toast.error(`Export failed: ${err.message}`);
199200
} finally {
200201
setExporting(false);
201202
}
@@ -206,6 +207,10 @@ export default function App() {
206207
setShowAuthModal(true);
207208
return;
208209
}
210+
if (!missionName.trim()) {
211+
toast.warning("Please enter a mission name before saving");
212+
return;
213+
}
209214
setSaving(true);
210215
try {
211216
if (missionId) {
@@ -228,7 +233,7 @@ export default function App() {
228233
}
229234
setDirty(false);
230235
} catch (err: any) {
231-
alert(`Save failed: ${err.message}`);
236+
toast.error(`Save failed: ${err.message}`);
232237
} finally {
233238
setSaving(false);
234239
}
@@ -254,7 +259,7 @@ export default function App() {
254259
pois: result.pois,
255260
});
256261
} catch (err: any) {
257-
alert(`Import failed: ${err.message}`);
262+
toast.error(`Import failed: ${err.message}`);
258263
}
259264

260265
// Reset file input
@@ -383,7 +388,7 @@ export default function App() {
383388
}
384389

385390
return (
386-
<div className="flex h-screen w-screen overflow-hidden bg-background">
391+
<div className="flex h-dvh w-screen overflow-hidden bg-background">
387392
{/* Sidebar */}
388393
<div className="w-88 flex flex-col border-r border-border bg-card shrink-0 tabular-nums">
389394
{/* Header */}
@@ -428,8 +433,9 @@ export default function App() {
428433
<Input
429434
value={missionName}
430435
onChange={(e) => setMissionName(e.target.value)}
431-
className="h-8 text-xs font-medium"
436+
className="h-8 text-xs font-medium border-blue-500/30 bg-blue-500/5 focus-visible:ring-blue-500/40"
432437
placeholder="Mission name"
438+
title="Name your mission for easy identification"
433439
/>
434440
</div>
435441

@@ -440,7 +446,7 @@ export default function App() {
440446
size="sm"
441447
onClick={handleSave}
442448
disabled={saving}
443-
className="flex-1 text-xs h-7"
449+
className="flex-1 text-xs h-7 border-blue-500/30 bg-blue-500/5 hover:bg-blue-500/15 hover:text-blue-300"
444450
title="Save mission to your account"
445451
>
446452
<Save className="h-3 w-3" />
@@ -451,8 +457,12 @@ export default function App() {
451457
size="sm"
452458
onClick={handleExport}
453459
disabled={exporting || waypoints.length < 2}
454-
className="flex-1 text-xs h-7"
455-
title="Export mission as DJI KMZ file"
460+
className="flex-1 text-xs h-7 border-blue-500/30 bg-blue-500/5 hover:bg-blue-500/15 hover:text-blue-300"
461+
title={
462+
waypoints.length < 2
463+
? "Add at least 2 waypoints to export"
464+
: "Export mission as DJI KMZ file"
465+
}
456466
>
457467
<Download className="h-3 w-3" />
458468
{exporting ? "..." : "Export KMZ"}
@@ -461,7 +471,7 @@ export default function App() {
461471
variant="outline"
462472
size="sm"
463473
onClick={() => fileInputRef.current?.click()}
464-
className="flex-1 text-xs h-7"
474+
className="flex-1 text-xs h-7 border-blue-500/30 bg-blue-500/5 hover:bg-blue-500/15 hover:text-blue-300"
465475
title="Import a DJI KMZ file"
466476
>
467477
<Upload className="h-3 w-3" />
@@ -481,8 +491,9 @@ export default function App() {
481491
{/* Waypoints section — BLUE accent */}
482492
<div className="border-l-2 border-blue-500/70 bg-blue-500/[0.03]">
483493
<button
484-
className="flex items-center gap-2 w-full px-3 py-2 text-xs font-semibold uppercase tracking-wider hover:bg-blue-500/10 text-blue-400"
494+
className="flex items-center gap-2 w-full px-3 py-2 text-xs font-semibold uppercase tracking-wider bg-blue-500/10 hover:bg-blue-500/15 text-blue-400"
485495
onClick={() => toggleSection("waypoints")}
496+
title="Flight path coordinates — click on the map to add"
486497
>
487498
{expandedSections.waypoints ? (
488499
<ChevronDown className="h-3 w-3" />
@@ -493,7 +504,7 @@ export default function App() {
493504
Waypoints ({waypoints.length})
494505
</button>
495506
{expandedSections.waypoints && (
496-
<div className="max-h-[40vh] overflow-y-auto">
507+
<div className="max-h-[40vh] overflow-y-auto section-expand">
497508
<WaypointList />
498509
</div>
499510
)}
@@ -502,8 +513,9 @@ export default function App() {
502513
{/* POIs section — AMBER/ORANGE accent */}
503514
<div className="border-l-2 border-amber-500/70 bg-amber-500/[0.03]">
504515
<button
505-
className="flex items-center gap-2 w-full px-3 py-2 text-xs font-semibold uppercase tracking-wider hover:bg-amber-500/10 text-amber-400"
516+
className="flex items-center gap-2 w-full px-3 py-2 text-xs font-semibold uppercase tracking-wider bg-amber-500/10 hover:bg-amber-500/15 text-amber-400"
506517
onClick={() => toggleSection("pois")}
518+
title="Targets the drone can point its camera at"
507519
>
508520
{expandedSections.pois ? (
509521
<ChevronDown className="h-3 w-3" />
@@ -514,7 +526,7 @@ export default function App() {
514526
Points of interest ({pois.length})
515527
</button>
516528
{expandedSections.pois && (
517-
<div className="max-h-[30vh] overflow-y-auto">
529+
<div className="max-h-[30vh] overflow-y-auto section-expand">
518530
<PoiList />
519531
</div>
520532
)}
@@ -523,8 +535,9 @@ export default function App() {
523535
{/* Obstacles section — RED accent */}
524536
<div className="border-l-2 border-red-500/70 bg-red-500/[0.03]">
525537
<button
526-
className="flex items-center gap-2 w-full px-3 py-2 text-xs font-semibold uppercase tracking-wider hover:bg-red-500/10 text-red-400"
538+
className="flex items-center gap-2 w-full px-3 py-2 text-xs font-semibold uppercase tracking-wider bg-red-500/10 hover:bg-red-500/15 text-red-400"
527539
onClick={() => toggleSection("obstacles")}
540+
title="No-fly zones to avoid during the mission"
528541
>
529542
{expandedSections.obstacles ? (
530543
<ChevronDown className="h-3 w-3" />
@@ -535,7 +548,7 @@ export default function App() {
535548
Obstacles ({obstacles.length})
536549
</button>
537550
{expandedSections.obstacles && (
538-
<div className="max-h-[30vh] overflow-y-auto">
551+
<div className="max-h-[30vh] overflow-y-auto section-expand">
539552
<ObstacleList />
540553
</div>
541554
)}
@@ -544,8 +557,9 @@ export default function App() {
544557
{/* Mission Settings section — PURPLE accent */}
545558
<div className="border-l-2 border-purple-500/70 bg-purple-500/[0.03]">
546559
<button
547-
className="flex items-center gap-2 w-full px-3 py-2 text-xs font-semibold uppercase tracking-wider hover:bg-purple-500/10 text-purple-400"
560+
className="flex items-center gap-2 w-full px-3 py-2 text-xs font-semibold uppercase tracking-wider bg-purple-500/10 hover:bg-purple-500/15 text-purple-400"
548561
onClick={() => toggleSection("config")}
562+
title="Drone model, speed, altitude and flight behavior"
549563
>
550564
{expandedSections.config ? (
551565
<ChevronDown className="h-3 w-3" />
@@ -556,7 +570,7 @@ export default function App() {
556570
Mission settings
557571
</button>
558572
{expandedSections.config && (
559-
<div className="max-h-[40vh] overflow-y-auto">
573+
<div className="max-h-[40vh] overflow-y-auto section-expand">
560574
<MissionConfig />
561575
</div>
562576
)}
@@ -612,64 +626,60 @@ export default function App() {
612626
})()}
613627
</div>
614628
<div className="flex items-center gap-3">
615-
{waypoints.length >= 2 && flightStats ? (
616-
(() => {
617-
const { distance, time } = flightStats;
618-
const elevGain = waypoints.reduce((sum, wp, i) => {
619-
if (i === 0) return 0;
620-
const diff = wp.height - waypoints[i - 1].height;
621-
return sum + (diff > 0 ? diff : 0);
622-
}, 0);
623-
const exceedsBattery = time > config.maxBatteryMinutes * 60;
624-
return (
625-
<>
626-
{elevGain > 0 && (
629+
{waypoints.length >= 2 && flightStats
630+
? (() => {
631+
const { distance, time } = flightStats;
632+
const elevGain = waypoints.reduce((sum, wp, i) => {
633+
if (i === 0) return 0;
634+
const diff = wp.height - waypoints[i - 1].height;
635+
return sum + (diff > 0 ? diff : 0);
636+
}, 0);
637+
const exceedsBattery = time > config.maxBatteryMinutes * 60;
638+
return (
639+
<>
640+
{elevGain > 0 && (
641+
<span
642+
className="flex items-center gap-1 text-[11px]"
643+
title="Elevation gain"
644+
>
645+
<TrendingUp className="h-3 w-3 text-orange-400" />
646+
<span className="text-orange-300 font-medium">
647+
{elevGain}m
648+
</span>
649+
</span>
650+
)}
627651
<span
628652
className="flex items-center gap-1 text-[11px]"
629-
title="Elevation gain"
653+
title="Total distance"
630654
>
631-
<TrendingUp className="h-3 w-3 text-orange-400" />
632-
<span className="text-orange-300 font-medium">
633-
{elevGain}m
655+
<Route className="h-3 w-3 text-emerald-400" />
656+
<span className="text-emerald-300 font-medium">
657+
{distance >= 1000
658+
? `${(distance / 1000).toFixed(1)}km`
659+
: `~${distance.toFixed(0)}m`}
634660
</span>
635661
</span>
636-
)}
637-
<span
638-
className="flex items-center gap-1 text-[11px]"
639-
title="Total distance"
640-
>
641-
<Route className="h-3 w-3 text-emerald-400" />
642-
<span className="text-emerald-300 font-medium">
643-
{distance >= 1000
644-
? `${(distance / 1000).toFixed(1)}km`
645-
: `~${distance.toFixed(0)}m`}
646-
</span>
647-
</span>
648-
<span
649-
className="flex items-center gap-1 text-[11px]"
650-
title={
651-
exceedsBattery
652-
? `Exceeds max battery (${config.maxBatteryMinutes}min)`
653-
: "Estimated flight time"
654-
}
655-
>
656-
<Clock
657-
className={`h-3 w-3 ${exceedsBattery ? "text-orange-400" : "text-yellow-400"}`}
658-
/>
659662
<span
660-
className={`font-medium ${exceedsBattery ? "text-orange-300" : "text-yellow-300"}`}
663+
className="flex items-center gap-1 text-[11px]"
664+
title={
665+
exceedsBattery
666+
? `Exceeds max battery (${config.maxBatteryMinutes}min)`
667+
: "Estimated flight time"
668+
}
661669
>
662-
{formatDuration(time)}
670+
<Clock
671+
className={`h-3 w-3 ${exceedsBattery ? "text-orange-400" : "text-yellow-400"}`}
672+
/>
673+
<span
674+
className={`font-medium ${exceedsBattery ? "text-orange-300" : "text-yellow-300"}`}
675+
>
676+
{formatDuration(time)}
677+
</span>
663678
</span>
664-
</span>
665-
</>
666-
);
667-
})()
668-
) : (
669-
<span className="text-[10px] text-muted-foreground">
670-
Add 2+ waypoints
671-
</span>
672-
)}
679+
</>
680+
);
681+
})()
682+
: null}
673683
</div>
674684
</div>
675685

@@ -727,13 +737,11 @@ export default function App() {
727737
<div className="h-6 w-6 rounded-full bg-muted flex items-center justify-center shrink-0">
728738
<User className="h-3 w-3 text-muted-foreground" />
729739
</div>
730-
Sign in to save missions
740+
Guest mode. Sign in to save missions
731741
</Button>
732742
)}
733743
</div>
734744
</div>
735-
736-
{/* Map */}
737745
<div className="flex-1 relative">
738746
<MapView />
739747
<BulkActionToolbar />

packages/frontend/src/components/map/MapView.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
MapContainer,
33
TileLayer,
4+
LayersControl,
45
useMapEvents,
56
useMap,
67
Polyline,
@@ -224,10 +225,20 @@ export function MapView() {
224225
className="h-full w-full z-0"
225226
zoomControl={false}
226227
>
227-
<TileLayer
228-
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
229-
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
230-
/>
228+
<LayersControl position="bottomleft">
229+
<LayersControl.BaseLayer checked name="Street">
230+
<TileLayer
231+
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
232+
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
233+
/>
234+
</LayersControl.BaseLayer>
235+
<LayersControl.BaseLayer name="Satellite">
236+
<TileLayer
237+
attribution='&copy; <a href="https://www.esri.com">Esri</a>'
238+
url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
239+
/>
240+
</LayersControl.BaseLayer>
241+
</LayersControl>
231242
<MapClickHandler />
232243
<ExposeMapInstance />
233244
<FitBoundsOnLoad />

0 commit comments

Comments
 (0)