Use user timezone to show dates on maps (#2020)

This commit is contained in:
Evgenii Burmakin 2025-12-08 22:12:17 +01:00 committed by GitHub
parent 1c9843dde7
commit 9ac4566b5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 37 additions and 23 deletions

View file

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## Fixed ## Fixed
- Cities visited during a trip are now being calculated correctly. #547 - 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 # [0.36.2] - 2025-12-06

View file

@ -7,7 +7,8 @@ export default class extends Controller {
static values = { static values = {
features: Object, features: Object,
userTheme: String userTheme: String,
timezone: String
} }
connect() { connect() {
@ -106,7 +107,8 @@ export default class extends Controller {
}); });
// Format timestamp for display // 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 // Create small tooltip that shows automatically
const tooltipContent = this.createTooltipContent(lastSeen, location.battery); const tooltipContent = this.createTooltipContent(lastSeen, location.battery);
@ -176,7 +178,8 @@ export default class extends Controller {
existingMarker.setIcon(newIcon); existingMarker.setIcon(newIcon);
// Update tooltip content // 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); const tooltipContent = this.createTooltipContent(lastSeen, locationData.battery);
existingMarker.setTooltipContent(tooltipContent); 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); const tooltipContent = this.createTooltipContent(lastSeen, location.battery);
familyMarker.bindTooltip(tooltipContent, { familyMarker.bindTooltip(tooltipContent, {

View file

@ -18,7 +18,7 @@ export class EventHandlers {
const content = ` const content = `
<div class="space-y-2"> <div class="space-y-2">
<div><span class="font-semibold">Time:</span> ${formatTimestamp(properties.timestamp)}</div> <div><span class="font-semibold">Time:</span> ${formatTimestamp(properties.timestamp, this.controller.timezoneValue)}</div>
${properties.battery ? `<div><span class="font-semibold">Battery:</span> ${properties.battery}%</div>` : ''} ${properties.battery ? `<div><span class="font-semibold">Battery:</span> ${properties.battery}%</div>` : ''}
${properties.altitude ? `<div><span class="font-semibold">Altitude:</span> ${Math.round(properties.altitude)}m</div>` : ''} ${properties.altitude ? `<div><span class="font-semibold">Altitude:</span> ${Math.round(properties.altitude)}m</div>` : ''}
${properties.velocity ? `<div><span class="font-semibold">Speed:</span> ${Math.round(properties.velocity)} km/h</div>` : ''} ${properties.velocity ? `<div><span class="font-semibold">Speed:</span> ${Math.round(properties.velocity)} km/h</div>` : ''}
@ -35,8 +35,8 @@ export class EventHandlers {
const feature = e.features[0] const feature = e.features[0]
const properties = feature.properties const properties = feature.properties
const startTime = formatTimestamp(properties.started_at) const startTime = formatTimestamp(properties.started_at, this.controller.timezoneValue)
const endTime = formatTimestamp(properties.ended_at) const endTime = formatTimestamp(properties.ended_at, this.controller.timezoneValue)
const durationHours = Math.round(properties.duration / 3600) const durationHours = Math.round(properties.duration / 3600)
const durationDisplay = durationHours >= 1 ? `${durationHours}h` : `${Math.round(properties.duration / 60)}m` const durationDisplay = durationHours >= 1 ? `${durationHours}h` : `${Math.round(properties.duration / 60)}m`
@ -70,7 +70,7 @@ export class EventHandlers {
const content = ` const content = `
<div class="space-y-2"> <div class="space-y-2">
${properties.photo_url ? `<img src="${properties.photo_url}" alt="Photo" class="w-full rounded-lg mb-2" />` : ''} ${properties.photo_url ? `<img src="${properties.photo_url}" alt="Photo" class="w-full rounded-lg mb-2" />` : ''}
${properties.taken_at ? `<div><span class="font-semibold">Taken:</span> ${formatTimestamp(properties.taken_at)}</div>` : ''} ${properties.taken_at ? `<div><span class="font-semibold">Taken:</span> ${formatTimestamp(properties.taken_at, this.controller.timezoneValue)}</div>` : ''}
</div> </div>
` `

View file

@ -26,7 +26,8 @@ export default class extends Controller {
static values = { static values = {
apiKey: String, apiKey: String,
startDate: String, startDate: String,
endDate: String endDate: String,
timezone: String
} }
static targets = [ static targets = [

View file

@ -2220,6 +2220,7 @@ export default class extends BaseController {
return; return;
} }
const timezone = this.timezone || 'UTC';
const html = citiesData.map(country => ` const html = citiesData.map(country => `
<div class="mb-4" style="min-width: min-content;"> <div class="mb-4" style="min-width: min-content;">
<h4 class="font-bold text-md">${country.country}</h4> <h4 class="font-bold text-md">${country.country}</h4>
@ -2228,7 +2229,7 @@ export default class extends BaseController {
<li class="text-sm whitespace-nowrap"> <li class="text-sm whitespace-nowrap">
${city.city} ${city.city}
<span class="text-gray-500"> <span class="text-gray-500">
(${new Date(city.timestamp * 1000).toLocaleDateString()}) (${new Date(city.timestamp * 1000).toLocaleDateString('en-US', { timeZone: timezone })})
</span> </span>
</li> </li>
`).join('')} `).join('')}

View file

@ -10,7 +10,8 @@ export default class extends BaseController {
uuid: String, uuid: String,
dataBounds: Object, dataBounds: Object,
hexagonsAvailable: Boolean, hexagonsAvailable: Boolean,
selfHosted: String selfHosted: String,
timezone: String
}; };
connect() { connect() {
@ -247,10 +248,11 @@ export default class extends BaseController {
} }
buildPopupContent(props) { buildPopupContent(props) {
const startDate = props.earliest_point ? new Date(props.earliest_point).toLocaleDateString() : 'N/A'; const timezone = this.timezoneValue || 'UTC';
const endDate = props.latest_point ? new Date(props.latest_point).toLocaleDateString() : 'N/A'; const startDate = props.earliest_point ? new Date(props.earliest_point).toLocaleDateString('en-US', { timeZone: timezone }) : 'N/A';
const startTime = props.earliest_point ? new Date(props.earliest_point).toLocaleTimeString() : ''; const endDate = props.latest_point ? new Date(props.latest_point).toLocaleDateString('en-US', { timeZone: timezone }) : 'N/A';
const endTime = props.latest_point ? new Date(props.latest_point).toLocaleTimeString() : ''; 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 ` return `
<div style="font-size: 12px; line-height: 1.6; max-width: 300px;"> <div style="font-size: 12px; line-height: 1.6; max-width: 300px;">

View file

@ -28,9 +28,10 @@ export function pointsToGeoJSON(points) {
/** /**
* Format timestamp for display * Format timestamp for display
* @param {number|string} timestamp - Unix timestamp (seconds) or ISO 8601 string * @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 * @returns {string} Formatted date/time
*/ */
export function formatTimestamp(timestamp) { export function formatTimestamp(timestamp, timezone = 'UTC') {
// Handle different timestamp formats // Handle different timestamp formats
let date let date
if (typeof timestamp === 'string') { if (typeof timestamp === 'string') {
@ -49,6 +50,7 @@ export function formatTimestamp(timestamp) {
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
hour: '2-digit', hour: '2-digit',
minute: '2-digit' minute: '2-digit',
timeZone: timezone
}) })
} }

View file

@ -17,12 +17,13 @@
data-tracks='<%= @tracks.to_json.html_safe %>' data-tracks='<%= @tracks.to_json.html_safe %>'
data-distance="<%= @distance %>" data-distance="<%= @distance %>"
data-points_number="<%= @points_number %>" data-points_number="<%= @points_number %>"
data-timezone="<%= Rails.configuration.time_zone %>" data-timezone="<%= current_user.timezone %>"
data-features='<%= @features.to_json.html_safe %>' 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-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-home_coordinates='<%= @home_coordinates.to_json.html_safe %>'
data-family-members-features-value='<%= @features.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 %>">
<div data-maps-target="container" class="w-full h-full"> <div data-maps-target="container" class="w-full h-full">
<div id="fog" class="fog"></div> <div id="fog" class="fog"></div>
</div> </div>

View file

@ -7,6 +7,7 @@
data-maps--maplibre-api-key-value="<%= current_user.api_key %>" data-maps--maplibre-api-key-value="<%= current_user.api_key %>"
data-maps--maplibre-start-date-value="<%= @start_at.to_s %>" data-maps--maplibre-start-date-value="<%= @start_at.to_s %>"
data-maps--maplibre-end-date-value="<%= @end_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" data-maps--maplibre-realtime-enabled-value="true"
style="width: 100%; height: 100%; position: relative;"> style="width: 100%; height: 100%; position: relative;">

View file

@ -63,7 +63,8 @@
data-public-stat-map-uuid-value="<%= @stat.sharing_uuid %>" 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-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-hexagons-available-value="<%= @hexagons_available.to_s %>"
data-public-stat-map-self-hosted-value="<%= @self_hosted %>"></div> data-public-stat-map-self-hosted-value="<%= @self_hosted %>"
data-public-stat-map-timezone-value="<%= @user.timezone %>"></div>
<!-- Loading overlay --> <!-- Loading overlay -->
<div id="map-loading" class="absolute inset-0 bg-base-200 bg-opacity-80 flex items-center justify-center z-50"> <div id="map-loading" class="absolute inset-0 bg-base-200 bg-opacity-80 flex items-center justify-center z-50">

View file

@ -22,7 +22,7 @@
data-path="<%= trip.path.to_json %>" data-path="<%= trip.path.to_json %>"
data-started_at="<%= trip.started_at %>" data-started_at="<%= trip.started_at %>"
data-ended_at="<%= trip.ended_at %>" data-ended_at="<%= trip.ended_at %>"
data-timezone="<%= Rails.configuration.time_zone %>"> data-timezone="<%= current_user.timezone %>">
</div> </div>
</div> </div>

View file

@ -9,7 +9,7 @@
data-path="<%= trip.path.coordinates.to_json %>" data-path="<%= trip.path.coordinates.to_json %>"
data-started_at="<%= trip.started_at %>" data-started_at="<%= trip.started_at %>"
data-ended_at="<%= trip.ended_at %>" data-ended_at="<%= trip.ended_at %>"
data-timezone="<%= Rails.configuration.time_zone %>"> data-timezone="<%= trip.user.timezone %>">
<div data-trips-target="container" class="h-[25rem] w-full min-h-screen md:h-64"> <div data-trips-target="container" class="h-[25rem] w-full min-h-screen md:h-64">
</div> </div>
</div> </div>

View file

@ -16,7 +16,7 @@
data-trip-map-path-value="<%= trip.path.coordinates.to_json %>" data-trip-map-path-value="<%= trip.path.coordinates.to_json %>"
data-trip-map-api-key-value="<%= current_user.api_key %>" 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-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 %>">
</div> </div>
</div> </div>
</div> </div>