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 ? `

` : ''}
- ${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 %>">