Skip to content

Commit b30d189

Browse files
committed
feat(Admin > Edit Album): Input lat,long spilt, clipboard, allow degrees minutes format
1 parent 1ca56d2 commit b30d189

File tree

1 file changed

+107
-13
lines changed

1 file changed

+107
-13
lines changed

src/components/AdminAlbum/Fields.tsx

Lines changed: 107 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Button from '@mui/joy/Button'
2+
import IconButton from '@mui/joy/IconButton'
23
import Input from '@mui/joy/Input'
34
import Option from '@mui/joy/Option'
45
import Select from '@mui/joy/Select'
@@ -16,6 +17,29 @@ const fetcher = (url: string) => fetch(url).then(r => r.json())
1617

1718
const REFERENCE_SOURCES: ItemReferenceSource[] = ['facebook', 'google', 'instagram', 'wikipedia', 'youtube']
1819

20+
// Parse DMS (Degrees Minutes Seconds) format to decimal
21+
// Supports formats like: 50° 22' 51.51" N or 50°22'51.51"N or 50 22 51.51 N
22+
function parseDMS(dms: string): number | null {
23+
const dmsPattern = /(\d+)[°\s]+(\d+)['\s]+(\d+(?:\.\d+)?)["\s]*([NSEW])?/i
24+
const match = dms.trim().match(dmsPattern)
25+
26+
if (!match) return null
27+
28+
const degrees = parseFloat(match[1])
29+
const minutes = parseFloat(match[2])
30+
const seconds = parseFloat(match[3])
31+
const direction = match[4]?.toUpperCase()
32+
33+
let decimal = degrees + minutes / 60 + seconds / 3600
34+
35+
// Negate if South or West
36+
if (direction === 'S' || direction === 'W') {
37+
decimal = -decimal
38+
}
39+
40+
return decimal
41+
}
42+
1943
export default function Fields(
2044
{ xmlAlbum, gallery, item, children, onItemUpdate, editedItems }:
2145
{
@@ -91,7 +115,7 @@ export default function Fields(
91115
...(geo && { geo }),
92116
...(ref && { ref }),
93117
}
94-
118+
95119
// Remove empty properties
96120
return removeEmpty(orderedItem)
97121
}).filter((item) => item !== undefined)
@@ -187,31 +211,101 @@ export default function Fields(
187211
<Input
188212
value={editedItem?.geo?.lat ?? ''}
189213
onChange={(e) => {
190-
const lat = e.target.value
191-
updateItem((prev: RawXmlItem | null) => prev ? {
192-
...prev,
193-
geo: lat || prev.geo?.lon ? { lat, lon: prev.geo?.lon || '', accuracy: prev.geo?.accuracy || '' } : undefined,
194-
} : null)
214+
const value = e.target.value
215+
216+
// Check if value contains DMS format (degrees, minutes, seconds)
217+
if (/\d+[°\s]+\d+['\s]+\d+/.test(value)) {
218+
// Split by comma to check for lat,lon DMS pair
219+
const parts = value.split(',').map(s => s.trim())
220+
221+
if (parts.length === 2) {
222+
// Parse both lat and lon as DMS
223+
const lat = parseDMS(parts[0])
224+
const lon = parseDMS(parts[1])
225+
if (lat !== null && lon !== null) {
226+
updateItem((prev: RawXmlItem | null) => prev ? {
227+
...prev,
228+
geo: { lat: lat.toString(), lon: lon.toString(), accuracy: prev.geo?.accuracy || '' },
229+
} : null)
230+
return
231+
}
232+
} else {
233+
// Single DMS value for latitude only
234+
const lat = parseDMS(value)
235+
if (lat !== null) {
236+
updateItem((prev: RawXmlItem | null) => prev ? {
237+
...prev,
238+
geo: { lat: lat.toString(), lon: prev.geo?.lon || '', accuracy: prev.geo?.accuracy || '' },
239+
} : null)
240+
return
241+
}
242+
}
243+
}
244+
245+
// Check if the value contains a comma (decimal lat,lon format)
246+
if (value.includes(',')) {
247+
const [lat, lon] = value.split(',').map(s => s.trim())
248+
updateItem((prev: RawXmlItem | null) => prev ? {
249+
...prev,
250+
geo: { lat: lat || '', lon: lon || '', accuracy: prev.geo?.accuracy || '' },
251+
} : null)
252+
} else {
253+
// Plain value for latitude
254+
const lat = value
255+
updateItem((prev: RawXmlItem | null) => prev ? {
256+
...prev,
257+
geo: lat || prev.geo?.lon ? { lat, lon: prev.geo?.lon || '', accuracy: prev.geo?.accuracy || '' } : undefined,
258+
} : null)
259+
}
195260
}}
196-
placeholder="Latitude"
197-
title="Latitude (geo.lat)"
198-
type="number"
261+
placeholder="Latitude (or lat,lon or DMS)"
262+
title="Latitude (geo.lat) - paste lat,lon or DMS format to auto-split"
199263
sx={{ flex: 1, minWidth: 0 }}
200264
/>
201265
<Input
202266
value={editedItem?.geo?.lon ?? ''}
203267
onChange={(e) => {
204-
const lon = e.target.value
268+
const value = e.target.value
269+
270+
// Check if value contains DMS format
271+
if (/\d+[°\s]+\d+['\s]+\d+/.test(value)) {
272+
const lon = parseDMS(value)
273+
if (lon !== null) {
274+
updateItem((prev: RawXmlItem | null) => prev ? {
275+
...prev,
276+
geo: { lat: prev.geo?.lat || '', lon: lon.toString(), accuracy: prev.geo?.accuracy || '' },
277+
} : null)
278+
return
279+
}
280+
}
281+
282+
// Plain value for longitude
283+
const lon = value
205284
updateItem((prev: RawXmlItem | null) => prev ? {
206285
...prev,
207286
geo: lon || prev.geo?.lat ? { lat: prev.geo?.lat || '', lon, accuracy: prev.geo?.accuracy || '' } : undefined,
208287
} : null)
209288
}}
210-
placeholder="Longitude"
211-
title="Longitude (geo.lon)"
212-
type="number"
289+
placeholder="Longitude (or DMS)"
290+
title="Longitude (geo.lon) - paste DMS format to convert"
213291
sx={{ flex: 1, minWidth: 0 }}
214292
/>
293+
<IconButton
294+
size="sm"
295+
variant="outlined"
296+
onClick={() => {
297+
const lat = editedItem?.geo?.lat ?? ''
298+
const lon = editedItem?.geo?.lon ?? ''
299+
if (lat && lon) {
300+
navigator.clipboard.writeText(`${lat},${lon}`)
301+
}
302+
}}
303+
disabled={!editedItem?.geo?.lat || !editedItem?.geo?.lon}
304+
title="Copy lat,lon to clipboard"
305+
sx={{ minWidth: 'auto', px: 1 }}
306+
>
307+
📋
308+
</IconButton>
215309
<Input
216310
value={editedItem?.geo?.accuracy ?? ''}
217311
onChange={(e) => {

0 commit comments

Comments
 (0)