From 9ac4566b5a222b17c35be77cf486a139c47bbd31 Mon Sep 17 00:00:00 2001 From: Evgenii Burmakin Date: Mon, 8 Dec 2025 22:12:17 +0100 Subject: [PATCH] Use user timezone to show dates on maps (#2020) --- CHANGELOG.md | 1 + .../controllers/family_members_controller.js | 12 ++++++++---- .../controllers/maps/maplibre/event_handlers.js | 8 ++++---- .../controllers/maps/maplibre_controller.js | 3 ++- app/javascript/controllers/maps_controller.js | 3 ++- .../controllers/public_stat_map_controller.js | 12 +++++++----- .../maps_maplibre/utils/geojson_transformers.js | 6 ++++-- app/views/map/leaflet/index.html.erb | 5 +++-- app/views/map/maplibre/index.html.erb | 1 + app/views/stats/public_month.html.erb | 3 ++- app/views/trips/_form.html.erb | 2 +- app/views/trips/_path.html.erb | 2 +- app/views/trips/_trip.html.erb | 2 +- 13 files changed, 37 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebd5e127..0c9f1351 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Fixed - Cities visited during a trip are now being calculated correctly. #547 +- Points on the map are now show time in user's timezone. #580 # [0.36.2] - 2025-12-06 diff --git a/app/javascript/controllers/family_members_controller.js b/app/javascript/controllers/family_members_controller.js index 9440d536..36a3db52 100644 --- a/app/javascript/controllers/family_members_controller.js +++ b/app/javascript/controllers/family_members_controller.js @@ -7,7 +7,8 @@ export default class extends Controller { static values = { features: Object, - userTheme: String + userTheme: String, + timezone: String } connect() { @@ -106,7 +107,8 @@ export default class extends Controller { }); // Format timestamp for display - const lastSeen = new Date(location.updated_at).toLocaleString(); + const timezone = this.timezoneValue || 'UTC'; + const lastSeen = new Date(location.updated_at).toLocaleString('en-US', { timeZone: timezone }); // Create small tooltip that shows automatically const tooltipContent = this.createTooltipContent(lastSeen, location.battery); @@ -176,7 +178,8 @@ export default class extends Controller { existingMarker.setIcon(newIcon); // Update tooltip content - const lastSeen = new Date(locationData.updated_at).toLocaleString(); + const timezone = this.timezoneValue || 'UTC'; + const lastSeen = new Date(locationData.updated_at).toLocaleString('en-US', { timeZone: timezone }); const tooltipContent = this.createTooltipContent(lastSeen, locationData.battery); existingMarker.setTooltipContent(tooltipContent); @@ -214,7 +217,8 @@ export default class extends Controller { }) }); - const lastSeen = new Date(location.updated_at).toLocaleString(); + const timezone = this.timezoneValue || 'UTC'; + const lastSeen = new Date(location.updated_at).toLocaleString('en-US', { timeZone: timezone }); const tooltipContent = this.createTooltipContent(lastSeen, location.battery); familyMarker.bindTooltip(tooltipContent, { diff --git a/app/javascript/controllers/maps/maplibre/event_handlers.js b/app/javascript/controllers/maps/maplibre/event_handlers.js index 812635d6..be214d13 100644 --- a/app/javascript/controllers/maps/maplibre/event_handlers.js +++ b/app/javascript/controllers/maps/maplibre/event_handlers.js @@ -18,7 +18,7 @@ export class EventHandlers { const content = `
-
Time: ${formatTimestamp(properties.timestamp)}
+
Time: ${formatTimestamp(properties.timestamp, this.controller.timezoneValue)}
${properties.battery ? `
Battery: ${properties.battery}%
` : ''} ${properties.altitude ? `
Altitude: ${Math.round(properties.altitude)}m
` : ''} ${properties.velocity ? `
Speed: ${Math.round(properties.velocity)} km/h
` : ''} @@ -35,8 +35,8 @@ export class EventHandlers { const feature = e.features[0] const properties = feature.properties - const startTime = formatTimestamp(properties.started_at) - const endTime = formatTimestamp(properties.ended_at) + const startTime = formatTimestamp(properties.started_at, this.controller.timezoneValue) + const endTime = formatTimestamp(properties.ended_at, this.controller.timezoneValue) const durationHours = Math.round(properties.duration / 3600) const durationDisplay = durationHours >= 1 ? `${durationHours}h` : `${Math.round(properties.duration / 60)}m` @@ -70,7 +70,7 @@ export class EventHandlers { const content = `
${properties.photo_url ? `Photo` : ''} - ${properties.taken_at ? `
Taken: ${formatTimestamp(properties.taken_at)}
` : ''} + ${properties.taken_at ? `
Taken: ${formatTimestamp(properties.taken_at, this.controller.timezoneValue)}
` : ''}
` diff --git a/app/javascript/controllers/maps/maplibre_controller.js b/app/javascript/controllers/maps/maplibre_controller.js index 5e416623..c9831830 100644 --- a/app/javascript/controllers/maps/maplibre_controller.js +++ b/app/javascript/controllers/maps/maplibre_controller.js @@ -26,7 +26,8 @@ export default class extends Controller { static values = { apiKey: String, startDate: String, - endDate: String + endDate: String, + timezone: String } static targets = [ diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index bf8b9653..8491ba8f 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -2220,6 +2220,7 @@ export default class extends BaseController { return; } + const timezone = this.timezone || 'UTC'; const html = citiesData.map(country => `

${country.country}

@@ -2228,7 +2229,7 @@ export default class extends BaseController {
  • ${city.city} - (${new Date(city.timestamp * 1000).toLocaleDateString()}) + (${new Date(city.timestamp * 1000).toLocaleDateString('en-US', { timeZone: timezone })})
  • `).join('')} diff --git a/app/javascript/controllers/public_stat_map_controller.js b/app/javascript/controllers/public_stat_map_controller.js index c218fd0c..3170d0e8 100644 --- a/app/javascript/controllers/public_stat_map_controller.js +++ b/app/javascript/controllers/public_stat_map_controller.js @@ -10,7 +10,8 @@ export default class extends BaseController { uuid: String, dataBounds: Object, hexagonsAvailable: Boolean, - selfHosted: String + selfHosted: String, + timezone: String }; connect() { @@ -247,10 +248,11 @@ export default class extends BaseController { } buildPopupContent(props) { - const startDate = props.earliest_point ? new Date(props.earliest_point).toLocaleDateString() : 'N/A'; - const endDate = props.latest_point ? new Date(props.latest_point).toLocaleDateString() : 'N/A'; - const startTime = props.earliest_point ? new Date(props.earliest_point).toLocaleTimeString() : ''; - const endTime = props.latest_point ? new Date(props.latest_point).toLocaleTimeString() : ''; + const timezone = this.timezoneValue || 'UTC'; + const startDate = props.earliest_point ? new Date(props.earliest_point).toLocaleDateString('en-US', { timeZone: timezone }) : 'N/A'; + const endDate = props.latest_point ? new Date(props.latest_point).toLocaleDateString('en-US', { timeZone: timezone }) : 'N/A'; + const startTime = props.earliest_point ? new Date(props.earliest_point).toLocaleTimeString('en-US', { timeZone: timezone }) : ''; + const endTime = props.latest_point ? new Date(props.latest_point).toLocaleTimeString('en-US', { timeZone: timezone }) : ''; return `
    diff --git a/app/javascript/maps_maplibre/utils/geojson_transformers.js b/app/javascript/maps_maplibre/utils/geojson_transformers.js index 9cfe30e6..72a1fd11 100644 --- a/app/javascript/maps_maplibre/utils/geojson_transformers.js +++ b/app/javascript/maps_maplibre/utils/geojson_transformers.js @@ -28,9 +28,10 @@ export function pointsToGeoJSON(points) { /** * Format timestamp for display * @param {number|string} timestamp - Unix timestamp (seconds) or ISO 8601 string + * @param {string} timezone - IANA timezone string (e.g., 'Europe/Berlin') * @returns {string} Formatted date/time */ -export function formatTimestamp(timestamp) { +export function formatTimestamp(timestamp, timezone = 'UTC') { // Handle different timestamp formats let date if (typeof timestamp === 'string') { @@ -49,6 +50,7 @@ export function formatTimestamp(timestamp) { month: 'short', day: 'numeric', hour: '2-digit', - minute: '2-digit' + minute: '2-digit', + timeZone: timezone }) } diff --git a/app/views/map/leaflet/index.html.erb b/app/views/map/leaflet/index.html.erb index 1086601b..08ea2110 100644 --- a/app/views/map/leaflet/index.html.erb +++ b/app/views/map/leaflet/index.html.erb @@ -17,12 +17,13 @@ data-tracks='<%= @tracks.to_json.html_safe %>' data-distance="<%= @distance %>" data-points_number="<%= @points_number %>" - data-timezone="<%= Rails.configuration.time_zone %>" + data-timezone="<%= current_user.timezone %>" data-features='<%= @features.to_json.html_safe %>' data-user_tags='<%= current_user.tags.ordered.select(:id, :name, :icon, :color).as_json.to_json.html_safe %>' data-home_coordinates='<%= @home_coordinates.to_json.html_safe %>' data-family-members-features-value='<%= @features.to_json.html_safe %>' - data-family-members-user-theme-value="<%= current_user&.theme || 'dark' %>"> + data-family-members-user-theme-value="<%= current_user&.theme || 'dark' %>" + data-family-members-timezone-value="<%= current_user.timezone %>">
    diff --git a/app/views/map/maplibre/index.html.erb b/app/views/map/maplibre/index.html.erb index 9d6bc8ac..18ac70f2 100644 --- a/app/views/map/maplibre/index.html.erb +++ b/app/views/map/maplibre/index.html.erb @@ -7,6 +7,7 @@ data-maps--maplibre-api-key-value="<%= current_user.api_key %>" data-maps--maplibre-start-date-value="<%= @start_at.to_s %>" data-maps--maplibre-end-date-value="<%= @end_at.to_s %>" + data-maps--maplibre-timezone-value="<%= current_user.timezone %>" data-maps--maplibre-realtime-enabled-value="true" style="width: 100%; height: 100%; position: relative;"> diff --git a/app/views/stats/public_month.html.erb b/app/views/stats/public_month.html.erb index 560d285f..7ca4ff69 100644 --- a/app/views/stats/public_month.html.erb +++ b/app/views/stats/public_month.html.erb @@ -63,7 +63,8 @@ data-public-stat-map-uuid-value="<%= @stat.sharing_uuid %>" data-public-stat-map-data-bounds-value="<%= @data_bounds.to_json if @data_bounds %>" data-public-stat-map-hexagons-available-value="<%= @hexagons_available.to_s %>" - data-public-stat-map-self-hosted-value="<%= @self_hosted %>">
    + data-public-stat-map-self-hosted-value="<%= @self_hosted %>" + data-public-stat-map-timezone-value="<%= @user.timezone %>">
    diff --git a/app/views/trips/_form.html.erb b/app/views/trips/_form.html.erb index 2d1420af..047f0956 100644 --- a/app/views/trips/_form.html.erb +++ b/app/views/trips/_form.html.erb @@ -22,7 +22,7 @@ data-path="<%= trip.path.to_json %>" data-started_at="<%= trip.started_at %>" data-ended_at="<%= trip.ended_at %>" - data-timezone="<%= Rails.configuration.time_zone %>"> + data-timezone="<%= current_user.timezone %>">
    diff --git a/app/views/trips/_path.html.erb b/app/views/trips/_path.html.erb index eb0679d2..b799e02f 100644 --- a/app/views/trips/_path.html.erb +++ b/app/views/trips/_path.html.erb @@ -9,7 +9,7 @@ data-path="<%= trip.path.coordinates.to_json %>" data-started_at="<%= trip.started_at %>" data-ended_at="<%= trip.ended_at %>" - data-timezone="<%= Rails.configuration.time_zone %>"> + data-timezone="<%= trip.user.timezone %>">
    diff --git a/app/views/trips/_trip.html.erb b/app/views/trips/_trip.html.erb index 87a226b5..2f70ee2b 100644 --- a/app/views/trips/_trip.html.erb +++ b/app/views/trips/_trip.html.erb @@ -16,7 +16,7 @@ data-trip-map-path-value="<%= trip.path.coordinates.to_json %>" data-trip-map-api-key-value="<%= current_user.api_key %>" data-trip-map-user-settings-value="<%= current_user.safe_settings.settings.to_json %>" - data-trip-map-timezone-value="<%= Rails.configuration.time_zone %>"> + data-trip-map-timezone-value="<%= trip.user.timezone %>">