From b740ca2293abbe4ab0e064f1ac7230166323be94 Mon Sep 17 00:00:00 2001 From: Charlie Date: Fri, 13 Feb 2026 22:59:29 +1000 Subject: [PATCH] refactor: restructure XML generator UI and workflow - Replaced search-based workflow with auto-load on mount - Restructured layout to two-column grid with map and sidebar - Added breadcrumb navigation for better flow - Added reset buttons for padding and altitude settings - Improved error handling with navigation redirects - Updated messaging across contribution pages - Changed XML filename format to use capitalized "Draft" --- src/pages/Contact.jsx | 2 +- src/pages/ContributeMap.jsx | 9 +- src/pages/ContributionDashboard.jsx | 2 +- src/pages/XMLGenerator.jsx | 521 ++++++++++++++-------------- 4 files changed, 262 insertions(+), 272 deletions(-) diff --git a/src/pages/Contact.jsx b/src/pages/Contact.jsx index f950b72..ec03ba1 100644 --- a/src/pages/Contact.jsx +++ b/src/pages/Contact.jsx @@ -289,7 +289,7 @@ const Contact = () => { {/* Toast notifications */} setShowSuccessToast(false)} diff --git a/src/pages/ContributeMap.jsx b/src/pages/ContributeMap.jsx index bb6b735..68611fc 100644 --- a/src/pages/ContributeMap.jsx +++ b/src/pages/ContributeMap.jsx @@ -1237,16 +1237,17 @@ const ContributeMap = () => {

- This airport currently has no lighting points submitted by the owning Division. - Please check back later, or contact the Division for support. + This airport currently has no airport lighting data submitted by the owning + Division. Please check back later, or contact the Division requesting this + airport.

) : (

- These are the existing mapped points for this airport, set by the Division. Your - contribution will add support for a specific simulator scenery package. + This is the existing airport data for this airport, set by the owning Division. + Your contribution will add support for a specific simulator scenery package.

)} diff --git a/src/pages/ContributionDashboard.jsx b/src/pages/ContributionDashboard.jsx index 64e9922..b261e04 100644 --- a/src/pages/ContributionDashboard.jsx +++ b/src/pages/ContributionDashboard.jsx @@ -303,7 +303,7 @@ const ContributionDashboard = () => {

Community Contributions

- Help expand the BARS network by contributing your own airport mappings + Help expand the BARS compatibility by contributing your own scenery contributions

{!user ? ( diff --git a/src/pages/XMLGenerator.jsx b/src/pages/XMLGenerator.jsx index 1534eea..0f0cf2f 100644 --- a/src/pages/XMLGenerator.jsx +++ b/src/pages/XMLGenerator.jsx @@ -1,23 +1,22 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; -import { useParams } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router-dom'; import { Layout } from '../components/layout/Layout'; import { Card } from '../components/shared/Card'; import { Button } from '../components/shared/Button'; import { Toast } from '../components/shared/Toast'; +import { Breadcrumb, BreadcrumbItem } from '../components/shared/Breadcrumb'; import { - Search, Loader, Download, AlertCircle, Info, - FileCode2, Copy, Check, Settings, ChevronDown, ChevronUp, Layers, - Map as MapIcon, + RotateCcw, } from 'lucide-react'; import Map, { Source, Layer, NavigationControl, ScaleControl } from 'react-map-gl/maplibre'; import 'maplibre-gl/dist/maplibre-gl.css'; @@ -205,13 +204,16 @@ const generateGUID = () => { return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; }; +const DEFAULT_PADDING_METERS = 1; +const DEFAULT_BASE_ALTITUDE = 0; + const XMLGenerator = () => { const { icao: urlIcao } = useParams(); + const navigate = useNavigate(); const mapRef = useRef(null); // State - const [icao, setIcao] = useState(urlIcao?.toUpperCase() || ''); - const [isSearching, setIsSearching] = useState(false); + const [icao] = useState(urlIcao?.toUpperCase() || ''); const [airport, setAirport] = useState(null); const [points, setPoints] = useState([]); const [loading, setLoading] = useState(false); @@ -219,8 +221,9 @@ const XMLGenerator = () => { const [showErrorToast, setShowErrorToast] = useState(false); const [generatedXML, setGeneratedXML] = useState(''); const [copied, setCopied] = useState(false); - const [paddingMeters, setPaddingMeters] = useState(1); - const [baseAltitude, setBaseAltitude] = useState(0); + const [paddingMeters, setPaddingMeters] = useState(DEFAULT_PADDING_METERS); + const [baseAltitude, setBaseAltitude] = useState(DEFAULT_BASE_ALTITUDE); + const [defaultElevation, setDefaultElevation] = useState(DEFAULT_BASE_ALTITUDE); const [showSettings, setShowSettings] = useState(false); const [mapStyle, setMapStyle] = useState(SATELLITE_STYLE); const [styleName, setStyleName] = useState('Satellite'); @@ -233,70 +236,54 @@ const XMLGenerator = () => { // Store generated polygon data for map display const [generatedPolygons, setGeneratedPolygons] = useState({ bars: [], remove: [] }); - // Auto-search if ICAO provided via URL + // Auto-load airport data on mount useEffect(() => { - if (urlIcao && /^[A-Za-z0-9]{4}$/.test(urlIcao)) { - handleSearch(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Search for airport - const handleSearch = async (e) => { - e?.preventDefault(); - - if (!icao) { - setError('Please enter an airport ICAO code'); - setShowErrorToast(true); - return; - } - - if (!/^[A-Za-z0-9]{4}$/.test(icao)) { - setError('ICAO code must be exactly 4 characters'); - setShowErrorToast(true); - return; - } - - setIsSearching(true); - setError(''); - setGeneratedXML(''); - setPoints([]); - - try { - const airportResponse = await fetch(`https://v2.stopbars.com/airports?icao=${icao}`); - if (!airportResponse.ok) { - throw new Error('Airport not found'); + const fetchData = async () => { + if (!urlIcao || !/^[A-Za-z0-9]{4}$/.test(urlIcao)) { + navigate('/contribute/new', { state: { error: 'airport_load_failed' } }); + return; } - const airportData = await airportResponse.json(); - - setAirport({ - icao: airportData.icao, - name: airportData.name, - latitude: airportData.latitude, - longitude: airportData.longitude, - elevation_m: airportData.elevation_m, - }); - // Set map view to airport location - setViewState((prev) => ({ - ...prev, - latitude: airportData.latitude, - longitude: airportData.longitude, - zoom: 14, - })); + setLoading(true); + setError(''); + setGeneratedXML(''); + setPoints([]); - if (typeof airportData.elevation_m === 'number') { - setBaseAltitude(airportData.elevation_m); - } + try { + const airportResponse = await fetch(`https://v2.stopbars.com/airports?icao=${urlIcao}`); + if (!airportResponse.ok) { + throw new Error('Failed to load airport data'); + } + const airportData = await airportResponse.json(); + + setAirport({ + icao: airportData.icao, + name: airportData.name, + latitude: airportData.latitude, + longitude: airportData.longitude, + elevation_m: airportData.elevation_m, + }); - const pointsResponse = await fetch(`https://v2.stopbars.com/airports/${icao}/points`); - if (!pointsResponse.ok) { - throw new Error('Failed to fetch airport points'); - } - const pointsData = await pointsResponse.json(); + // Set map view to airport location + setViewState((prev) => ({ + ...prev, + latitude: airportData.latitude, + longitude: airportData.longitude, + zoom: 14, + })); + + if (typeof airportData.elevation_m === 'number') { + setBaseAltitude(airportData.elevation_m); + setDefaultElevation(airportData.elevation_m); + } - setPoints( - pointsData.map((point) => ({ + const pointsResponse = await fetch(`https://v2.stopbars.com/airports/${urlIcao}/points`); + if (!pointsResponse.ok) { + throw new Error('Failed to fetch airport points'); + } + const pointsData = await pointsResponse.json(); + + const transformedPoints = pointsData.map((point) => ({ id: point.id, type: point.type, name: point.name, @@ -305,16 +292,31 @@ const XMLGenerator = () => { color: point.color || undefined, elevated: point.elevated, ihp: point.ihp, - })) - ); - } catch (err) { - setError(err.message || 'Failed to load airport data'); - setShowErrorToast(true); - setAirport(null); - } finally { - setIsSearching(false); - } - }; + })); + + // Redirect to map if no points found + if (transformedPoints.length === 0) { + navigate(`/contribute/map/${urlIcao}`); + return; + } + + setPoints(transformedPoints); + } catch (err) { + console.error(err); + navigate('/contribute/new', { state: { error: 'airport_load_failed' } }); + return; + } finally { + setLoading(false); + } + }; + + fetchData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Reset functions + const resetPadding = () => setPaddingMeters(DEFAULT_PADDING_METERS); + const resetAltitude = () => setBaseAltitude(defaultElevation); // Generate XML from points const generateXML = useCallback(() => { @@ -424,7 +426,7 @@ ${allPolygonXML.join('\n')} const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `${airport?.icao || 'airport'}-draft.xml`; + a.download = `${airport?.icao || 'airport'}-Draft.xml`; document.body.appendChild(a); a.click(); document.body.removeChild(a); @@ -495,175 +497,41 @@ ${allPolygonXML.join('\n')} [generatedPolygons.remove] ); - return ( - -
-
- {/* Header */} -
-

MSFS XML Generator

-

- Generate draft MSFS scenery XML files from BARS airport lighting data. These files - will require manual adjustments before submitting as a contribution. -

-
- - {/* Search Bar */} -
-
-
- setIcao(e.target.value.toUpperCase())} - placeholder="Enter ICAO code (e.g., EGLL)" - maxLength={4} - className="w-full pl-12 pr-4 py-3 bg-zinc-800 border border-zinc-700 rounded-lg focus:outline-none focus:border-blue-500 text-white placeholder:text-zinc-500 uppercase tracking-wider" - /> - + if (loading) { + return ( + +
+
+
+
+
- - +
+
+
+ ); + } - {/* Results */} - {airport && ( - <> - {/* Airport Info Card */} - -
-
-

{airport.icao}

-

{airport.name}

-
- -
- - {/* Settings Panel */} - {showSettings && ( -
-
-
- - - setPaddingMeters(Math.max(1, parseInt(e.target.value) || 1)) - } - min={1} - max={50} - className="w-full px-4 py-2 bg-zinc-900 border border-zinc-700 rounded-lg focus:outline-none focus:border-blue-500" - /> -
-
- - setBaseAltitude(parseFloat(e.target.value) || 0)} - step={0.1} - className="w-full px-4 py-2 bg-zinc-900 border border-zinc-700 rounded-lg focus:outline-none focus:border-blue-500" - /> -
-
- -
- )} - - {/* Statistics */} - {generatedXML && ( -
-
-
{points.length}
-
Objects Loaded
-
-
-
{barsCount}
-
BARS Polygons
-
-
-
{removeCount}
-
Remove Areas
-
-
- )} - - {/* No points warning */} - {points.length === 0 && !isSearching && ( -
- -

- No points found for this airport. The division may not have added lighting - data yet. -

-
- )} + return ( + +
+
+
+
+ + + + +
+
- {/* Download Actions */} - {generatedXML && ( -
- - -
- )} - - - {/* Map Preview */} - {generatedXML && ( - -
-
- -

Map Preview

-
- {polygonCount} total polygons -
-
+
+
+ {/* Map */} +
+ {airport ? ( + <> {styleName}
+ + ) : ( +
+
+ +

Failed to load airport data

+
- - )} + )} +
+
+ +
+ {airport && ( + <> + {/* XML Statistics */} + {generatedXML && ( + +
+
+

{airport.icao}

+

{airport.name}

+
+ +
+
+
+ Objects Loaded + {points.length} +
+
+ BARS Polygons + {barsCount} +
+
+ Remove Areas + {removeCount} +
+
+ Total Polygons + {polygonCount} +
+
- {/* Info Notice */} - {generatedXML && ( -
- -

- This is a draft XML file. You may need to adjust altitudes and fine-tune polygon - positions in your scenery editor for best results. -

-
+ {/* Settings Section */} + {showSettings && ( +
+
+
+ + +
+ + setPaddingMeters(Math.max(1, parseInt(e.target.value) || 1)) + } + min={1} + max={50} + className="w-full px-4 py-2 bg-zinc-900 border border-zinc-700 rounded-lg focus:outline-none focus:border-blue-500" + /> +
+
+
+ + +
+ setBaseAltitude(parseFloat(e.target.value) || 0)} + step={0.1} + className="w-full px-4 py-2 bg-zinc-900 border border-zinc-700 rounded-lg focus:outline-none focus:border-blue-500" + /> +
+
+ )} +
+ )} + + {/* Download Actions */} + {generatedXML && ( + +
+ + +
+
+ )} + + {/* Info Notice */} + {generatedXML && ( +
+ +

+ This is a draft XML generated from existing airport lighting data. You will + need to adjust and fine-tune polygon positions in your scenery for best + results. This file is a draft and should not be used for a final submission. +

+
+ )} + )} - - )} - - {/* Empty State */} - {!airport && !isSearching && ( - - -

No Airport Selected

-

- Enter an ICAO code above to generate XML for an airport -

-
- )} +
+