diff --git a/frontend/src/components/sidebar/BulkTakenAtEditor.tsx b/frontend/src/components/sidebar/BulkTakenAtEditor.tsx index e5b689b..7de3a00 100644 --- a/frontend/src/components/sidebar/BulkTakenAtEditor.tsx +++ b/frontend/src/components/sidebar/BulkTakenAtEditor.tsx @@ -64,7 +64,11 @@ export function BulkTakenAtEditor({ const handleApplyUniform = () => { if (!uniformDraft) return - const parsed = new Date(uniformDraft) + // uniformDraft is "YYYY-MM-DD" from a date input; anchor to local + // midnight so the stored ISO matches the day the user picked when + // viewed in their own timezone. + const [year, month, day] = uniformDraft.split('-').map(Number) + const parsed = new Date(year, month - 1, day) if (Number.isNaN(parsed.getTime())) return onApplyUniform(parsed.toISOString()) } @@ -74,7 +78,7 @@ export function BulkTakenAtEditor({ {/* Apply-one row */}
setUniformDraft(e.target.value)} disabled={disabled} diff --git a/frontend/src/components/sidebar/PhotoInfoPanel.tsx b/frontend/src/components/sidebar/PhotoInfoPanel.tsx index 129ed99..75bed06 100644 --- a/frontend/src/components/sidebar/PhotoInfoPanel.tsx +++ b/frontend/src/components/sidebar/PhotoInfoPanel.tsx @@ -39,7 +39,7 @@ import { COLOR_LABEL_OPTIONS, type ColorLabel, } from '../../constants/colorLabels' -import { toDatetimeLocalValue } from '../../lib/guessDateFromPath' +import { toDateInputValue } from '../../lib/guessDateFromPath' import { TagsEditor } from './TagsEditor' import { TakenAtEditor } from './TakenAtEditor' @@ -311,7 +311,7 @@ export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelPro setFilenameDraft(photo?.filename ?? '') setNotesDraft(photo?.user_notes ?? '') setTakenAtDraft( - photo?.taken_at ? toDatetimeLocalValue(new Date(photo.taken_at)) : '' + photo?.taken_at ? toDateInputValue(new Date(photo.taken_at)) : '' ) }, [ photo?.id, @@ -353,26 +353,32 @@ export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelPro updateMutation.mutate({ user_notes: next || null }) } - /** Commit a datetime-local draft back to the server. The backend also + /** Commit a date-only draft back to the server. The backend also * rewrites EXIF on disk, so a failure here rolls the draft back to the * server value — we never want the UI to silently disagree with the * file. An empty string is a no-op because the input's `required` is - * off and we don't yet have a "clear date" affordance. */ + * off and we don't yet have a "clear date" affordance. + * + * Comparison is YYYY-MM-DD vs YYYY-MM-DD (not full ISO) so a blur with + * no edits doesn't clobber the stored time-of-day with a new local + * midnight. */ const commitTakenAt = (rawValue?: string) => { const source = rawValue ?? takenAtDraft if (!source) return - const parsed = new Date(source) - if (Number.isNaN(parsed.getTime())) { + const [y, m, d] = source.split('-').map((n) => Number(n)) + if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) { toast.error('Invalid date', 'Could not parse the value') setTakenAtDraft( - photo?.taken_at ? toDatetimeLocalValue(new Date(photo.taken_at)) : '' + photo?.taken_at ? toDateInputValue(new Date(photo.taken_at)) : '' ) return } + const current = photo?.taken_at + ? toDateInputValue(new Date(photo.taken_at)) + : '' + if (current === source) return + const parsed = new Date(y, m - 1, d) const iso = parsed.toISOString() - if (photo?.taken_at && new Date(photo.taken_at).toISOString() === iso) { - return - } updateMutation.mutate( { taken_at: iso }, { @@ -383,7 +389,7 @@ export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelPro ) setTakenAtDraft( photo?.taken_at - ? toDatetimeLocalValue(new Date(photo.taken_at)) + ? toDateInputValue(new Date(photo.taken_at)) : '' ) }, @@ -455,22 +461,6 @@ export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelPro } />
- {/* Filepath: full-width mono so it wraps cleanly instead of - * stretching the two-column grid. */} -
- Path -

- {photo.filepath - ? photo.filepath.replace( - /^\/?nextcloud-users\/[^/]+\/files\//, - '…/', - ) - : '—'} -

-
{hasGps && ( +
+ Path +

+ {photo.filepath + ? photo.filepath.replace( + /^\/?nextcloud-users\/[^/]+\/files\//, + '…/', + ) + : '—'} +

+
+ Date Taken
onDraftChange(e.target.value)} onBlur={() => onCommit()} @@ -88,7 +88,7 @@ export function TakenAtEditor({ } else if (e.key === 'Escape') { onDraftChange( photo.taken_at - ? toDatetimeLocalValue(new Date(photo.taken_at)) + ? toDateInputValue(new Date(photo.taken_at)) : '' ) e.currentTarget.blur() @@ -108,7 +108,7 @@ export function TakenAtEditor({ {showSuggestion && guess && (