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. */}
-