feat: English UI + more distinct isochrone colors
- Translate all UI strings to English (labels, toasts, hints, context menu) - Replace monochromatic color palettes with visually distinct multi-hue palettes per transport mode (car: yellow→red→purple, bike: cyan→indigo→green, walk: green→lime alternating) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -61,7 +61,7 @@ function App(): React.JSX.Element {
|
|||||||
<div className="splash-ring" />
|
<div className="splash-ring" />
|
||||||
<div className="splash-ring" />
|
<div className="splash-ring" />
|
||||||
</div>
|
</div>
|
||||||
<p>Démarrage du moteur de routage...</p>
|
<p>Starting routing engine...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -71,8 +71,8 @@ function App(): React.JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<div className="splash splash-error">
|
<div className="splash splash-error">
|
||||||
<div className="splash-content">
|
<div className="splash-content">
|
||||||
<p>Valhalla n'a pas pu démarrer.</p>
|
<p>Valhalla could not start.</p>
|
||||||
<p className="splash-hint">Vérifiez que Docker est actif dans WSL Ubuntu.</p>
|
<p className="splash-hint">Check that Docker is running in WSL Ubuntu.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import type { FeatureCollection, Feature, Polygon, MultiPolygon } from 'geojson'
|
|||||||
import type { TransportMode } from '../store/useAppStore'
|
import type { TransportMode } from '../store/useAppStore'
|
||||||
|
|
||||||
const ISOCHRONE_COLORS: Record<TransportMode, string[]> = {
|
const ISOCHRONE_COLORS: Record<TransportMode, string[]> = {
|
||||||
pedestrian: ['#86efac', '#4ade80', '#22c55e', '#16a34a', '#15803d', '#166534', '#14532d', '#052e16'],
|
pedestrian: ['#bbf7d0', '#4ade80', '#16a34a', '#166534', '#a3e635', '#65a30d', '#bef264', '#4d7c0f'],
|
||||||
bicycle: ['#67e8f9', '#22d3ee', '#06b6d4', '#0891b2', '#0e7490', '#155e75', '#164e63', '#083344'],
|
bicycle: ['#a5f3fc', '#22d3ee', '#0891b2', '#155e75', '#818cf8', '#4338ca', '#6ee7b7', '#047857'],
|
||||||
auto: ['#fcd34d', '#fbbf24', '#f59e0b', '#d97706', '#b45309', '#92400e', '#78350f', '#451a03'],
|
auto: ['#fef08a', '#fbbf24', '#f97316', '#dc2626', '#e879f9', '#9333ea', '#fb7185', '#be123c'],
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getIsochroneColors(mode: TransportMode): string[] {
|
export function getIsochroneColors(mode: TransportMode): string[] {
|
||||||
@@ -111,14 +111,14 @@ export async function fetchIsochrones(
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if ((err as Error).name === 'AbortError') throw err
|
if ((err as Error).name === 'AbortError') throw err
|
||||||
console.error('[Valhalla] fetch error:', err)
|
console.error('[Valhalla] fetch error:', err)
|
||||||
throw new Error(`Erreur réseau: ${err}`)
|
throw new Error(`Network error: ${err}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Valhalla] Response status:', response.status)
|
console.log('[Valhalla] Response status:', response.status)
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errText = await response.text().catch(() => '')
|
const errText = await response.text().catch(() => '')
|
||||||
let msg = `Erreur Valhalla ${response.status}`
|
let msg = `Valhalla error ${response.status}`
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(errText)
|
const parsed = JSON.parse(errText)
|
||||||
if (parsed.error) msg = `Valhalla ${parsed.error_code}: ${parsed.error}`
|
if (parsed.error) msg = `Valhalla ${parsed.error_code}: ${parsed.error}`
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { exportPng, exportGeoJSON } from '../utils/export'
|
|||||||
import { TimeRangeEditor } from './TimeRangeEditor'
|
import { TimeRangeEditor } from './TimeRangeEditor'
|
||||||
|
|
||||||
const MODES: { value: TransportMode; label: string; icon: string }[] = [
|
const MODES: { value: TransportMode; label: string; icon: string }[] = [
|
||||||
{ value: 'auto', label: 'Voiture', icon: '🚗' },
|
{ value: 'auto', label: 'Car', icon: '🚗' },
|
||||||
{ value: 'bicycle', label: 'Vélo', icon: '🚴' },
|
{ value: 'bicycle', label: 'Bike', icon: '🚴' },
|
||||||
{ value: 'pedestrian', label: 'Piéton', icon: '🚶' }
|
{ value: 'pedestrian', label: 'Walk', icon: '🚶' }
|
||||||
]
|
]
|
||||||
|
|
||||||
interface ControlPanelProps {
|
interface ControlPanelProps {
|
||||||
@@ -52,7 +52,7 @@ export function ControlPanel({ mapRef }: ControlPanelProps): React.JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleCalculate = async (): Promise<void> => {
|
const handleCalculate = async (): Promise<void> => {
|
||||||
if (!point) { setError('Cliquez sur la carte pour définir un point de départ.'); return }
|
if (!point) { setError('Click on the map to set a starting point.'); return }
|
||||||
cancelFetch()
|
cancelFetch()
|
||||||
setError(null)
|
setError(null)
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -61,7 +61,7 @@ export function ControlPanel({ mapRef }: ControlPanelProps): React.JSX.Element {
|
|||||||
const data = await fetchIsochrones(point, mode, timeRanges)
|
const data = await fetchIsochrones(point, mode, timeRanges)
|
||||||
setIsochrones(data)
|
setIsochrones(data)
|
||||||
const elapsed = ((Date.now() - t0) / 1000).toFixed(1)
|
const elapsed = ((Date.now() - t0) / 1000).toFixed(1)
|
||||||
addToast({ message: `Calculé en ${elapsed}s`, type: 'success', duration: 3000 })
|
addToast({ message: `Computed in ${elapsed}s`, type: 'success', duration: 3000 })
|
||||||
addToHistory({ point, mode, timeRanges, isochrones: data, timestamp: Date.now(), label: query || undefined })
|
addToHistory({ point, mode, timeRanges, isochrones: data, timestamp: Date.now(), label: query || undefined })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = (e as Error).message
|
const msg = (e as Error).message
|
||||||
@@ -83,7 +83,7 @@ export function ControlPanel({ mapRef }: ControlPanelProps): React.JSX.Element {
|
|||||||
return () => document.removeEventListener('keydown', handler)
|
return () => document.removeEventListener('keydown', handler)
|
||||||
}, [point, mode, timeRanges, loading]) // eslint-disable-line react-hooks/exhaustive-deps
|
}, [point, mode, timeRanges, loading]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const statusLabel = valhallaStatus === 'ready' ? 'Moteur prêt' : valhallaStatus === 'starting' ? 'Démarrage...' : 'Erreur moteur'
|
const statusLabel = valhallaStatus === 'ready' ? 'Engine ready' : valhallaStatus === 'starting' ? 'Starting...' : 'Engine error'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="control-panel">
|
<aside className="control-panel">
|
||||||
@@ -94,12 +94,12 @@ export function ControlPanel({ mapRef }: ControlPanelProps): React.JSX.Element {
|
|||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<section className="panel-section">
|
<section className="panel-section">
|
||||||
<label className="section-label">Point de départ</label>
|
<label className="section-label">Starting point</label>
|
||||||
<div className="search-wrap">
|
<div className="search-wrap">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="input-text"
|
className="input-text"
|
||||||
placeholder="Rechercher un lieu..."
|
placeholder="Search for a place..."
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => handleQueryChange(e.target.value)}
|
onChange={(e) => handleQueryChange(e.target.value)}
|
||||||
onFocus={() => results.length > 0 && setShowDropdown(true)}
|
onFocus={() => results.length > 0 && setShowDropdown(true)}
|
||||||
@@ -118,22 +118,22 @@ export function ControlPanel({ mapRef }: ControlPanelProps): React.JSX.Element {
|
|||||||
<span className="coord-text">📍 {point[1].toFixed(5)}, {point[0].toFixed(5)}</span>
|
<span className="coord-text">📍 {point[1].toFixed(5)}, {point[0].toFixed(5)}</span>
|
||||||
<button
|
<button
|
||||||
className="btn-copy"
|
className="btn-copy"
|
||||||
title="Copier les coordonnées"
|
title="Copy coordinates"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(`${point[1].toFixed(6)}, ${point[0].toFixed(6)}`)
|
navigator.clipboard.writeText(`${point[1].toFixed(6)}, ${point[0].toFixed(6)}`)
|
||||||
addToast({ message: 'Coordonnées copiées', type: 'info', duration: 2000 })
|
addToast({ message: 'Coordinates copied', type: 'info', duration: 2000 })
|
||||||
}}
|
}}
|
||||||
>⎘</button>
|
>⎘</button>
|
||||||
<button className="btn-clear" title="Effacer le point" onClick={() => { setPoint(null); setIsochrones(null); setQuery('') }}>✕</button>
|
<button className="btn-clear" title="Clear point" onClick={() => { setPoint(null); setIsochrones(null); setQuery('') }}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="hint">ou cliquez directement sur la carte</p>
|
<p className="hint">or click directly on the map</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Transport mode */}
|
{/* Transport mode */}
|
||||||
<section className="panel-section">
|
<section className="panel-section">
|
||||||
<label className="section-label">Mode de transport</label>
|
<label className="section-label">Transport mode</label>
|
||||||
<div className="mode-selector">
|
<div className="mode-selector">
|
||||||
{MODES.map((m) => (
|
{MODES.map((m) => (
|
||||||
<button
|
<button
|
||||||
@@ -151,14 +151,14 @@ export function ControlPanel({ mapRef }: ControlPanelProps): React.JSX.Element {
|
|||||||
|
|
||||||
{/* Time ranges */}
|
{/* Time ranges */}
|
||||||
<section className="panel-section">
|
<section className="panel-section">
|
||||||
<label className="section-label">Durées de trajet (heures)</label>
|
<label className="section-label">Travel times (hours)</label>
|
||||||
<TimeRangeEditor />
|
<TimeRangeEditor />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Calculate */}
|
{/* Calculate */}
|
||||||
<section className="panel-section">
|
<section className="panel-section">
|
||||||
<button className="btn-primary" onClick={handleCalculate} disabled={loading}>
|
<button className="btn-primary" onClick={handleCalculate} disabled={loading}>
|
||||||
{loading ? '⏳ Calcul en cours...' : '⚡ Calculer les isochrones'}
|
{loading ? '⏳ Computing...' : '⚡ Compute isochrones'}
|
||||||
</button>
|
</button>
|
||||||
<label className="auto-recalc-toggle">
|
<label className="auto-recalc-toggle">
|
||||||
<input
|
<input
|
||||||
@@ -166,13 +166,13 @@ export function ControlPanel({ mapRef }: ControlPanelProps): React.JSX.Element {
|
|||||||
checked={autoRecalculate}
|
checked={autoRecalculate}
|
||||||
onChange={(e) => setAutoRecalculate(e.target.checked)}
|
onChange={(e) => setAutoRecalculate(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<span>Recalcul automatique</span>
|
<span>Auto-recalculate</span>
|
||||||
</label>
|
</label>
|
||||||
{error && (
|
{error && (
|
||||||
<div className="error-card">
|
<div className="error-card">
|
||||||
<span className="error-icon">⚠</span>
|
<span className="error-icon">⚠</span>
|
||||||
<div className="error-body">
|
<div className="error-body">
|
||||||
<p className="error-title">Erreur de calcul</p>
|
<p className="error-title">Computation error</p>
|
||||||
<p className="error-detail">{error}</p>
|
<p className="error-detail">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
<button className="btn-retry" onClick={handleCalculate}>↺</button>
|
<button className="btn-retry" onClick={handleCalculate}>↺</button>
|
||||||
@@ -184,6 +184,7 @@ export function ControlPanel({ mapRef }: ControlPanelProps): React.JSX.Element {
|
|||||||
{isochrones && (
|
{isochrones && (
|
||||||
<section className="panel-section">
|
<section className="panel-section">
|
||||||
<label className="section-label">Export</label>
|
<label className="section-label">Export</label>
|
||||||
|
|
||||||
<div className="export-row">
|
<div className="export-row">
|
||||||
<button className="btn-secondary" onClick={() => mapRef.current && exportPng(mapRef.current)}>📸 PNG</button>
|
<button className="btn-secondary" onClick={() => mapRef.current && exportPng(mapRef.current)}>📸 PNG</button>
|
||||||
<button className="btn-secondary" onClick={() => exportGeoJSON(isochrones)}>📄 GeoJSON</button>
|
<button className="btn-secondary" onClick={() => exportGeoJSON(isochrones)}>📄 GeoJSON</button>
|
||||||
@@ -195,16 +196,16 @@ export function ControlPanel({ mapRef }: ControlPanelProps): React.JSX.Element {
|
|||||||
{history.length > 0 && (
|
{history.length > 0 && (
|
||||||
<section className="panel-section">
|
<section className="panel-section">
|
||||||
<button className="history-toggle" onClick={() => setHistoryOpen((v) => !v)}>
|
<button className="history-toggle" onClick={() => setHistoryOpen((v) => !v)}>
|
||||||
<span>Historique</span>
|
<span>History</span>
|
||||||
<span>{historyOpen ? '▾' : '▸'}</span>
|
<span>{historyOpen ? '▾' : '▸'}</span>
|
||||||
</button>
|
</button>
|
||||||
<div className={`collapsible-body${historyOpen ? ' open' : ''}`}>
|
<div className={`collapsible-body${historyOpen ? ' open' : ''}`}>
|
||||||
<div className="history-list">
|
<div className="history-list">
|
||||||
{history.map((h) => (
|
{history.map((h) => (
|
||||||
<button key={h.id} className="history-item" onClick={() => { restoreHistory(h); addToast({ message: 'Calcul restauré', type: 'info', duration: 2000 }) }}>
|
<button key={h.id} className="history-item" onClick={() => { restoreHistory(h); addToast({ message: 'Calculation restored', type: 'info', duration: 2000 }) }}>
|
||||||
<span className="history-icon">{MODES.find((m) => m.value === h.mode)?.icon}</span>
|
<span className="history-icon">{MODES.find((m) => m.value === h.mode)?.icon}</span>
|
||||||
<span className="history-label">{h.label ?? `${h.point[1].toFixed(3)}, ${h.point[0].toFixed(3)}`}</span>
|
<span className="history-label">{h.label ?? `${h.point[1].toFixed(3)}, ${h.point[0].toFixed(3)}`}</span>
|
||||||
<span className="history-meta">{h.timeRanges.length} zones</span>
|
<span className="history-meta">{h.timeRanges.length} range{h.timeRanges.length > 1 ? 's' : ''}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function Legend(): React.JSX.Element | null {
|
|||||||
key={t}
|
key={t}
|
||||||
className={`legend-item${hiddenLayers.has(i) ? ' hidden' : ''}`}
|
className={`legend-item${hiddenLayers.has(i) ? ' hidden' : ''}`}
|
||||||
onClick={() => toggleLayer(i)}
|
onClick={() => toggleLayer(i)}
|
||||||
title={hiddenLayers.has(i) ? 'Afficher' : 'Masquer'}
|
title={hiddenLayers.has(i) ? 'Show' : 'Hide'}
|
||||||
>
|
>
|
||||||
<span className="legend-swatch" style={{ background: colors[i % colors.length] }} />
|
<span className="legend-swatch" style={{ background: colors[i % colors.length] }} />
|
||||||
<span className="legend-label">{formatDuration(t)}</span>
|
<span className="legend-label">{formatDuration(t)}</span>
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export function MapView({ mapRef }: MapViewProps): React.JSX.Element {
|
|||||||
setPoint([contextMenu.lng, contextMenu.lat])
|
setPoint([contextMenu.lng, contextMenu.lat])
|
||||||
setContextMenu(null)
|
setContextMenu(null)
|
||||||
}}>
|
}}>
|
||||||
📍 Définir comme point de départ
|
📍 Set as starting point
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -54,12 +54,12 @@ export function TimeRangeEditor(): React.JSX.Element {
|
|||||||
onChange={(e) => update(i, parseFloat(e.target.value) || 0.5)}
|
onChange={(e) => update(i, parseFloat(e.target.value) || 0.5)}
|
||||||
/>
|
/>
|
||||||
<span className="range-preview">h — {formatDuration(t)}</span>
|
<span className="range-preview">h — {formatDuration(t)}</span>
|
||||||
<button className="btn-icon" onClick={() => remove(i)} disabled={sorted.length <= 1} title="Supprimer">×</button>
|
<button className="btn-icon" onClick={() => remove(i)} disabled={sorted.length <= 1} title="Remove">×</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{sorted.length < 8 && Math.max(...sorted) < 36000 && (
|
{sorted.length < 8 && Math.max(...sorted) < 36000 && (
|
||||||
<button className="btn-add" onClick={add}>+ Ajouter une durée</button>
|
<button className="btn-add" onClick={add}>+ Add a duration</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user