Modernize stack (React 19, Vite, Node 24) and add map preview features
This commit is contained in:
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM node:18-alpine
|
||||
FROM node:24-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -13,7 +13,7 @@ COPY . .
|
||||
EXPOSE 3000
|
||||
|
||||
# Set environment variable for API URL
|
||||
ENV REACT_APP_API_URL=http://localhost:3001/api
|
||||
ENV VITE_API_URL=http://localhost:3001/api
|
||||
|
||||
# Start development server
|
||||
CMD ["npm", "start"]
|
||||
|
||||
@@ -11,5 +11,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+3291
File diff suppressed because it is too large
Load Diff
+26
-11
@@ -3,20 +3,35 @@
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"maplibre-gl": "^3.6.2",
|
||||
"axios": "^1.6.2"
|
||||
"axios": "^1.7.9",
|
||||
"maplibre-gl": "^5.20.1",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.6.0",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"jsdom": "^29.0.0",
|
||||
"vite": "^6.0.0",
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [">0.2%", "not dead", "not op_mini all"],
|
||||
"development": ["last 1 chrome version", "last 1 firefox version", "last 1 safari version"]
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import './styles/App.css';
|
||||
const APP = () => {
|
||||
const mapContainerRef = useRef(null);
|
||||
const mapRef = useRef(null);
|
||||
const startMarkerRef = useRef(null);
|
||||
const [selectedPoint, setSelectedPoint] = useState({ lat: 51.5074, lon: -0.1278 });
|
||||
const [direction, setDirection] = useState(45);
|
||||
const [lineOfSightData, setLineOfSightData] = useState(null);
|
||||
@@ -26,13 +27,47 @@ const APP = () => {
|
||||
|
||||
mapRef.current.on('load', () => {
|
||||
console.log('Map loaded successfully');
|
||||
|
||||
// Initialize start marker
|
||||
startMarkerRef.current = new maplibregl.Marker({ color: '#FF6B6B' })
|
||||
.setLngLat([selectedPoint.lon, selectedPoint.lat])
|
||||
.addTo(mapRef.current);
|
||||
|
||||
// Initialize preview line source and layer
|
||||
mapRef.current.addSource('preview-line', {
|
||||
type: 'geojson',
|
||||
data: {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'LineString',
|
||||
coordinates: []
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mapRef.current.addLayer({
|
||||
id: 'preview-line',
|
||||
type: 'line',
|
||||
source: 'preview-line',
|
||||
paint: {
|
||||
'line-color': '#FF6B6B',
|
||||
'line-width': 2,
|
||||
'line-dasharray': [2, 2]
|
||||
}
|
||||
});
|
||||
|
||||
updatePreviewLine(selectedPoint, direction);
|
||||
});
|
||||
|
||||
mapRef.current.on('click', (e) => {
|
||||
const { lng, lat } = e.lngLat;
|
||||
setSelectedPoint({ lat, lon: lng });
|
||||
|
||||
// Clear previous line
|
||||
if (startMarkerRef.current) {
|
||||
startMarkerRef.current.setLngLat([lng, lat]);
|
||||
}
|
||||
|
||||
// Clear previous final line when moving start point
|
||||
if (mapRef.current.getSource('line-of-sight')) {
|
||||
mapRef.current.removeLayer('line-of-sight');
|
||||
mapRef.current.removeSource('line-of-sight');
|
||||
@@ -44,6 +79,61 @@ const APP = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Update preview whenever point or direction changes
|
||||
if (mapRef.current && mapRef.current.isStyleLoaded()) {
|
||||
updatePreviewLine(selectedPoint, direction);
|
||||
}
|
||||
}, [selectedPoint, direction]);
|
||||
|
||||
const updatePreviewLine = (point, bearing) => {
|
||||
const map = mapRef.current;
|
||||
if (!map || !map.getSource('preview-line')) return;
|
||||
|
||||
// Generate 50 points along a 5000km great circle path for a smooth curve
|
||||
const path = [];
|
||||
const steps = 50;
|
||||
const totalDistance = 5000;
|
||||
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const dist = (totalDistance * i) / steps;
|
||||
path.push(calculateDestination(point.lat, point.lon, bearing, dist));
|
||||
}
|
||||
|
||||
map.getSource('preview-line').setData({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'LineString',
|
||||
coordinates: path.map(p => [p.lon, p.lat])
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Helper to calculate destination point given start, bearing, and distance (km)
|
||||
const calculateDestination = (lat, lon, bearing, distance) => {
|
||||
const R = 6371; // Earth's radius in km
|
||||
const brng = (bearing * Math.PI) / 180;
|
||||
const φ1 = (lat * Math.PI) / 180;
|
||||
const λ1 = (lon * Math.PI) / 180;
|
||||
const δ = distance / R;
|
||||
|
||||
const φ2 = Math.asin(
|
||||
Math.sin(φ1) * Math.cos(δ) +
|
||||
Math.cos(φ1) * Math.sin(δ) * Math.cos(brng)
|
||||
);
|
||||
const λ2 =
|
||||
λ1 +
|
||||
Math.atan2(
|
||||
Math.sin(brng) * Math.sin(δ) * Math.cos(φ1),
|
||||
Math.cos(δ) - Math.sin(φ1) * Math.sin(φ2)
|
||||
);
|
||||
|
||||
return {
|
||||
lat: (φ2 * 180) / Math.PI,
|
||||
lon: (((λ2 * 180) / Math.PI + 540) % 360) - 180
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Update map style when toggle changes
|
||||
if (mapRef.current) {
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3001/api';
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
build: {
|
||||
outDir: 'build',
|
||||
target: 'esnext',
|
||||
},
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
target: 'esnext',
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user