fix(gps): erreur explicite + saisie manuelle si GPS indisponible (HTTP/laptop)
navigator.geolocation est undefined sur HTTP hors localhost (contexte non sécurisé). - Message d'erreur visible selon le cas (permission, HTTPS, timeout) - Fallback : deux champs lat/lon s'affichent automatiquement - TodoForm : bouton GPS toujours actif (plus disabled sur navigator.geolocation) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,8 @@ export default function NoteForm({ initialValues, onSubmit, onCancel, submitLabe
|
|||||||
const [gpsLat, setGpsLat] = useState<number | undefined>(initialValues?.gps_lat ?? undefined)
|
const [gpsLat, setGpsLat] = useState<number | undefined>(initialValues?.gps_lat ?? undefined)
|
||||||
const [gpsLon, setGpsLon] = useState<number | undefined>(initialValues?.gps_lon ?? undefined)
|
const [gpsLon, setGpsLon] = useState<number | undefined>(initialValues?.gps_lon ?? undefined)
|
||||||
const [gpsLoading, setGpsLoading] = useState(false)
|
const [gpsLoading, setGpsLoading] = useState(false)
|
||||||
|
const [gpsError, setGpsError] = useState<string | null>(null)
|
||||||
|
const [gpsManual, setGpsManual] = useState(false)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const contentRef = useRef<HTMLTextAreaElement>(null)
|
const contentRef = useRef<HTMLTextAreaElement>(null)
|
||||||
@@ -39,19 +41,37 @@ export default function NoteForm({ initialValues, onSubmit, onCancel, submitLabe
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleGps() {
|
function handleGps() {
|
||||||
if (!navigator.geolocation) return
|
setGpsError(null)
|
||||||
|
if (!navigator.geolocation) {
|
||||||
|
setGpsError('GPS non disponible (HTTPS requis hors localhost)')
|
||||||
|
setGpsManual(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
setGpsLoading(true)
|
setGpsLoading(true)
|
||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
pos => {
|
pos => {
|
||||||
setGpsLat(pos.coords.latitude)
|
setGpsLat(pos.coords.latitude)
|
||||||
setGpsLon(pos.coords.longitude)
|
setGpsLon(pos.coords.longitude)
|
||||||
setGpsLoading(false)
|
setGpsLoading(false)
|
||||||
|
setGpsManual(false)
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
setGpsLoading(false)
|
||||||
|
if (err.code === 1) setGpsError('Permission refusée')
|
||||||
|
else if (err.code === 2) setGpsError('Position indisponible — saisie manuelle ?')
|
||||||
|
else setGpsError('Délai dépassé — saisie manuelle ?')
|
||||||
|
setGpsManual(true)
|
||||||
},
|
},
|
||||||
() => setGpsLoading(false),
|
|
||||||
{ timeout: 8000 },
|
{ timeout: 8000 },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleManualCoord(field: 'lat' | 'lon', val: string) {
|
||||||
|
const n = parseFloat(val.replace(',', '.'))
|
||||||
|
if (field === 'lat') setGpsLat(isNaN(n) ? undefined : n)
|
||||||
|
else setGpsLon(isNaN(n) ? undefined : n)
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (!content.trim()) return
|
if (!content.trim()) return
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
@@ -112,36 +132,57 @@ export default function NoteForm({ initialValues, onSubmit, onCancel, submitLabe
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* GPS */}
|
{/* GPS */}
|
||||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
<button
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||||
type="button"
|
<button
|
||||||
onClick={handleGps}
|
type="button"
|
||||||
disabled={gpsLoading}
|
onClick={handleGps}
|
||||||
style={{
|
disabled={gpsLoading}
|
||||||
padding: '6px 14px',
|
style={{
|
||||||
borderRadius: 8,
|
padding: '6px 14px', borderRadius: 8,
|
||||||
border: '1px solid var(--bg-5)',
|
border: '1px solid var(--bg-5)',
|
||||||
background: gpsLat ? 'var(--info)' : 'var(--bg-4)',
|
background: gpsLat != null ? 'var(--info)' : 'var(--bg-4)',
|
||||||
color: gpsLat ? '#fff' : 'var(--ink-2)',
|
color: gpsLat != null ? '#fff' : 'var(--ink-2)',
|
||||||
cursor: 'pointer',
|
cursor: gpsLoading ? 'default' : 'pointer',
|
||||||
fontFamily: 'var(--font-ui)',
|
fontFamily: 'var(--font-ui)', fontSize: 13, minHeight: 36,
|
||||||
fontSize: 13,
|
}}
|
||||||
minHeight: 36,
|
>
|
||||||
}}
|
<i className={`fa-solid fa-location-dot`} style={{ marginRight: 6 }} />
|
||||||
>
|
{gpsLoading ? '…' : gpsLat != null ? 'GPS capturé' : 'Ajouter GPS'}
|
||||||
{gpsLoading ? '…' : gpsLat ? '📍 GPS capturé' : '📍 Ajouter GPS'}
|
</button>
|
||||||
</button>
|
{gpsLat != null && (
|
||||||
{gpsLat && (
|
<>
|
||||||
<>
|
<span style={{ color: 'var(--ink-3)', fontSize: 11, fontFamily: 'var(--font-mono)', flex: 1 }}>
|
||||||
<span style={{ color: 'var(--ink-3)', fontSize: 11, fontFamily: 'var(--font-mono)' }}>
|
{gpsLat.toFixed(5)}, {gpsLon?.toFixed(5)}
|
||||||
{gpsLat.toFixed(5)}, {gpsLon?.toFixed(5)}
|
</span>
|
||||||
</span>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
onClick={() => { setGpsLat(undefined); setGpsLon(undefined); setGpsManual(false); setGpsError(null) }}
|
||||||
onClick={() => { setGpsLat(undefined); setGpsLon(undefined) }}
|
style={{ background: 'transparent', border: 'none', color: 'var(--err)', cursor: 'pointer', fontSize: 14 }}
|
||||||
style={{ background: 'transparent', border: 'none', color: 'var(--err)', cursor: 'pointer', fontSize: 14 }}
|
>✕</button>
|
||||||
>✕</button>
|
</>
|
||||||
</>
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{gpsError && (
|
||||||
|
<span style={{ color: 'var(--warn)', fontSize: 11, fontFamily: 'var(--font-ui)' }}>
|
||||||
|
{gpsError}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{gpsManual && gpsLat == null && (
|
||||||
|
<div style={{ display: 'flex', gap: 6 }}>
|
||||||
|
<input
|
||||||
|
style={{ ...inputStyle, flex: 1, fontSize: 12 }}
|
||||||
|
placeholder="Latitude (ex: 48.8566)"
|
||||||
|
onChange={e => handleManualCoord('lat', e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
style={{ ...inputStyle, flex: 1, fontSize: 12 }}
|
||||||
|
placeholder="Longitude (ex: 2.3522)"
|
||||||
|
onChange={e => handleManualCoord('lon', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export default function TodoForm({ onSubmit, onCancel, initialValues, submitLabe
|
|||||||
const [photoPath, setPhotoPath] = useState<string | undefined>(initialValues?.photo_path ?? undefined)
|
const [photoPath, setPhotoPath] = useState<string | undefined>(initialValues?.photo_path ?? undefined)
|
||||||
const [photoLoading, setPhotoLoading] = useState(false)
|
const [photoLoading, setPhotoLoading] = useState(false)
|
||||||
const [gpsLoading, setGpsLoading] = useState(false)
|
const [gpsLoading, setGpsLoading] = useState(false)
|
||||||
|
const [gpsError, setGpsError] = useState<string | null>(null)
|
||||||
|
const [gpsManual, setGpsManual] = useState(false)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const fileRef = useRef<HTMLInputElement>(null)
|
const fileRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
@@ -109,18 +111,37 @@ export default function TodoForm({ onSubmit, onCancel, initialValues, submitLabe
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
function handleGps() {
|
function handleGps() {
|
||||||
if (!navigator.geolocation) return
|
setGpsError(null)
|
||||||
|
if (!navigator.geolocation) {
|
||||||
|
setGpsError('GPS non disponible (HTTPS requis hors localhost)')
|
||||||
|
setGpsManual(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
setGpsLoading(true)
|
setGpsLoading(true)
|
||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
pos => {
|
pos => {
|
||||||
setGpsLat(pos.coords.latitude)
|
setGpsLat(pos.coords.latitude)
|
||||||
setGpsLng(pos.coords.longitude)
|
setGpsLng(pos.coords.longitude)
|
||||||
setGpsLoading(false)
|
setGpsLoading(false)
|
||||||
|
setGpsManual(false)
|
||||||
},
|
},
|
||||||
() => setGpsLoading(false)
|
err => {
|
||||||
|
setGpsLoading(false)
|
||||||
|
if (err.code === 1) setGpsError('Permission refusée')
|
||||||
|
else if (err.code === 2) setGpsError('Position indisponible — saisie manuelle ?')
|
||||||
|
else setGpsError('Délai dépassé — saisie manuelle ?')
|
||||||
|
setGpsManual(true)
|
||||||
|
},
|
||||||
|
{ timeout: 8000 },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleManualCoord(field: 'lat' | 'lng', val: string) {
|
||||||
|
const n = parseFloat(val.replace(',', '.'))
|
||||||
|
if (field === 'lat') setGpsLat(isNaN(n) ? undefined : n)
|
||||||
|
else setGpsLng(isNaN(n) ? undefined : n)
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!title.trim()) return
|
if (!title.trim()) return
|
||||||
@@ -296,7 +317,7 @@ export default function TodoForm({ onSubmit, onCancel, initialValues, submitLabe
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleGps}
|
onClick={handleGps}
|
||||||
disabled={!navigator.geolocation}
|
disabled={gpsLoading}
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: '8px',
|
padding: '8px',
|
||||||
@@ -306,7 +327,7 @@ export default function TodoForm({ onSubmit, onCancel, initialValues, submitLabe
|
|||||||
color: gpsLat != null ? '#fff' : 'var(--ink-3)',
|
color: gpsLat != null ? '#fff' : 'var(--ink-3)',
|
||||||
fontFamily: 'var(--font-ui)',
|
fontFamily: 'var(--font-ui)',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
cursor: navigator.geolocation ? 'pointer' : 'default',
|
cursor: gpsLoading ? 'default' : 'pointer',
|
||||||
minHeight: 40,
|
minHeight: 40,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -318,6 +339,25 @@ export default function TodoForm({ onSubmit, onCancel, initialValues, submitLabe
|
|||||||
ou Ctrl+V pour coller une image
|
ou Ctrl+V pour coller une image
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{gpsError && (
|
||||||
|
<span style={{ color: 'var(--warn)', fontSize: 11, fontFamily: 'var(--font-ui)', paddingLeft: 4 }}>
|
||||||
|
{gpsError}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{gpsManual && gpsLat == null && (
|
||||||
|
<div style={{ display: 'flex', gap: 6 }}>
|
||||||
|
<input
|
||||||
|
style={{ ...inputStyle, flex: 1, fontSize: 12 }}
|
||||||
|
placeholder="Latitude (ex: 48.8566)"
|
||||||
|
onChange={e => handleManualCoord('lat', e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
style={{ ...inputStyle, flex: 1, fontSize: 12 }}
|
||||||
|
placeholder="Longitude (ex: 2.3522)"
|
||||||
|
onChange={e => handleManualCoord('lng', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Boutons */}
|
{/* Boutons */}
|
||||||
|
|||||||
Reference in New Issue
Block a user