Add images for stats page backgrounds

This commit is contained in:
Eugene Burmakin 2025-09-12 20:11:14 +02:00
parent 612c30026c
commit 5ff35136f2
21 changed files with 111 additions and 77 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

View file

@ -92,7 +92,7 @@
} }
.loading-spinner::before { .loading-spinner::before {
content: '🔵'; content: '';
font-size: 18px; font-size: 18px;
animation: spinner 1s linear infinite; animation: spinner 1s linear infinite;
} }

View file

@ -71,7 +71,7 @@ module StatsHelper
date = Date.new(stat.year, stat.month, peak[0]) date = Date.new(stat.year, stat.month, peak[0])
distance_km = (peak[1] / 1000).round(2) distance_km = (peak[1] / 1000).round(2)
distance_unit = current_user.safe_settings.distance_unit distance_unit = stat.user.safe_settings.distance_unit
distance_value = distance_value =
if distance_unit == 'mi' if distance_unit == 'mi'
@ -90,7 +90,7 @@ module StatsHelper
# Create a hash with date as key and distance as value # Create a hash with date as key and distance as value
distance_by_date = stat.daily_distance.to_h.transform_keys do |timestamp| distance_by_date = stat.daily_distance.to_h.transform_keys do |timestamp|
Time.at(timestamp).in_time_zone(current_user.timezone || 'UTC').to_date Time.at(timestamp).in_time_zone(stat.user.timezone || 'UTC').to_date
end end
# Initialize variables to track the quietest week # Initialize variables to track the quietest week
@ -161,4 +161,21 @@ module StatsHelper
when 12 then 'bg-gradient-to-tl from-blue-600 to-blue-700' # Winter dark blue when 12 then 'bg-gradient-to-tl from-blue-600 to-blue-700' # Winter dark blue
end end
end end
def month_bg_image(stat)
case stat.month
when 1 then image_url('backgrounds/months/anne-nygard-VwzfdVT6_9s-unsplash.jpg')
when 2 then image_url('backgrounds/months/ainars-cekuls-buAAKQiMfoI-unsplash.jpg')
when 3 then image_url('backgrounds/months/ahmad-hasan-xEYWelDHYF0-unsplash.jpg')
when 4 then image_url('backgrounds/months/lily-Rg1nSqXNPN4-unsplash.jpg')
when 5 then image_url('backgrounds/months/milan-de-clercq-YtllSzi2JLY-unsplash.jpg')
when 6 then image_url('backgrounds/months/liana-mikah-6B05zlnPOEc-unsplash.jpg')
when 7 then image_url('backgrounds/months/irina-iriser-fKAl8Oid6zM-unsplash.jpg')
when 8 then image_url('backgrounds/months/nadiia-ploshchenko-ZnDtJaIec_E-unsplash.jpg')
when 9 then image_url('backgrounds/months/gracehues-photography-AYtup7uqimA-unsplash.jpg')
when 10 then image_url('backgrounds/months/babi-hdNa4GCCgbg-unsplash.jpg')
when 11 then image_url('backgrounds/months/foto-phanatic-8LaUOtP-de4-unsplash.jpg')
when 12 then image_url('backgrounds/months/henry-schneider-FqKPySIaxuE-unsplash.jpg')
end
end
end end

View file

@ -4,14 +4,15 @@ import { createHexagonGrid } from "../maps/hexagon_grid";
export default class extends Controller { export default class extends Controller {
static targets = ["container"]; static targets = ["container"];
static values = { static values = {
year: Number, year: Number,
month: Number, month: Number,
uuid: String, uuid: String,
dataBounds: Object dataBounds: Object
}; };
connect() { connect() {
console.log('🏁 Controller connected - loading overlay should be visible');
this.initializeMap(); this.initializeMap();
this.loadHexagons(); this.loadHexagons();
} }
@ -47,31 +48,33 @@ export default class extends Controller {
} }
async loadHexagons() { async loadHexagons() {
try { console.log('🎯 loadHexagons started - checking overlay state');
// Calculate date range for the month const initialLoadingElement = document.getElementById('map-loading');
const startDate = new Date(this.yearValue, this.monthValue - 1, 1); console.log('📊 Initial overlay display:', initialLoadingElement?.style.display || 'default');
const endDate = new Date(this.yearValue, this.monthValue, 0, 23, 59, 59);
try {
// Use server-provided data bounds // Use server-provided data bounds
const dataBounds = this.dataBoundsValue; const dataBounds = this.dataBoundsValue;
if (dataBounds && dataBounds.point_count > 0) { if (dataBounds && dataBounds.point_count > 0) {
// Set map view to data bounds BEFORE creating hexagon grid // Set map view to data bounds BEFORE creating hexagon grid
this.map.fitBounds([ this.map.fitBounds([
[dataBounds.min_lat, dataBounds.min_lng], [dataBounds.min_lat, dataBounds.min_lng],
[dataBounds.max_lat, dataBounds.max_lng] [dataBounds.max_lat, dataBounds.max_lng]
], { padding: [20, 20] }); ], { padding: [20, 20] });
// Wait for the map to finish fitting bounds // Wait for the map to finish fitting bounds
console.log('⏳ About to wait for map moveend - overlay should still be visible');
await new Promise(resolve => { await new Promise(resolve => {
this.map.once('moveend', resolve); this.map.once('moveend', resolve);
// Fallback timeout in case moveend doesn't fire // Fallback timeout in case moveend doesn't fire
setTimeout(resolve, 1000); setTimeout(resolve, 1000);
}); });
console.log('✅ Map fitBounds complete - checking overlay state');
const afterFitBoundsElement = document.getElementById('map-loading');
console.log('📊 After fitBounds overlay display:', afterFitBoundsElement?.style.display || 'default');
} }
// Create hexagon grid with API endpoint for public sharing
// Note: We need to prevent automatic showing during init
this.hexagonGrid = createHexagonGrid(this.map, { this.hexagonGrid = createHexagonGrid(this.map, {
apiEndpoint: '/api/v1/maps/hexagons', apiEndpoint: '/api/v1/maps/hexagons',
style: { style: {
@ -94,40 +97,64 @@ export default class extends Controller {
this.map.off('zoomend'); this.map.off('zoomend');
// Load hexagons only once on page load (static behavior) // Load hexagons only once on page load (static behavior)
// NOTE: Do NOT hide loading overlay here - let loadStaticHexagons() handle it
if (dataBounds && dataBounds.point_count > 0) { if (dataBounds && dataBounds.point_count > 0) {
await this.loadStaticHexagons(); await this.loadStaticHexagons();
} else { } else {
console.warn('No data bounds or points available - not showing hexagons'); console.warn('No data bounds or points available - not showing hexagons');
} // Only hide loading indicator if no hexagons to load
const loadingElement = document.getElementById('map-loading');
// Hide loading indicator if (loadingElement) {
const loadingElement = document.getElementById('map-loading'); loadingElement.style.display = 'none';
if (loadingElement) { }
loadingElement.style.display = 'none';
} }
} catch (error) { } catch (error) {
console.error('Error initializing hexagon grid:', error); console.error('Error initializing hexagon grid:', error);
// Hide loading indicator even on error // Hide loading indicator on initialization error
const loadingElement = document.getElementById('map-loading'); const loadingElement = document.getElementById('map-loading');
if (loadingElement) { if (loadingElement) {
loadingElement.style.display = 'none'; loadingElement.style.display = 'none';
} }
} }
// Do NOT hide loading overlay here - let loadStaticHexagons() handle it completely
} }
async loadStaticHexagons() { async loadStaticHexagons() {
console.log('🔄 Loading static hexagons for public sharing...'); console.log('🔄 Loading static hexagons for public sharing...');
// Ensure loading overlay is visible and disable map interaction
const loadingElement = document.getElementById('map-loading');
console.log('🔍 Loading element found:', !!loadingElement);
if (loadingElement) {
loadingElement.style.display = 'flex';
loadingElement.style.visibility = 'visible';
loadingElement.style.zIndex = '9999';
console.log('👁️ Loading overlay ENSURED visible - should be visible now');
}
// Disable map interaction during loading
this.map.dragging.disable();
this.map.touchZoom.disable();
this.map.doubleClickZoom.disable();
this.map.scrollWheelZoom.disable();
this.map.boxZoom.disable();
this.map.keyboard.disable();
if (this.map.tap) this.map.tap.disable();
// Add delay to ensure loading overlay is visible
await new Promise(resolve => setTimeout(resolve, 500));
try { try {
// Calculate date range for the month // Calculate date range for the month
const startDate = new Date(this.yearValue, this.monthValue - 1, 1); const startDate = new Date(this.yearValue, this.monthValue - 1, 1);
const endDate = new Date(this.yearValue, this.monthValue, 0, 23, 59, 59); const endDate = new Date(this.yearValue, this.monthValue, 0, 23, 59, 59);
// Use the full data bounds for hexagon request (not current map viewport) // Use the full data bounds for hexagon request (not current map viewport)
const dataBounds = this.dataBoundsValue; const dataBounds = this.dataBoundsValue;
const params = new URLSearchParams({ const params = new URLSearchParams({
min_lon: dataBounds.min_lng, min_lon: dataBounds.min_lng,
min_lat: dataBounds.min_lat, min_lat: dataBounds.min_lat,
@ -141,7 +168,7 @@ export default class extends Controller {
const url = `/api/v1/maps/hexagons?${params}`; const url = `/api/v1/maps/hexagons?${params}`;
console.log('📍 Fetching static hexagons from:', url); console.log('📍 Fetching static hexagons from:', url);
const response = await fetch(url, { const response = await fetch(url, {
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -164,6 +191,22 @@ export default class extends Controller {
} catch (error) { } catch (error) {
console.error('Failed to load static hexagons:', error); console.error('Failed to load static hexagons:', error);
} finally {
// Re-enable map interaction after loading (success or failure)
this.map.dragging.enable();
this.map.touchZoom.enable();
this.map.doubleClickZoom.enable();
this.map.scrollWheelZoom.enable();
this.map.boxZoom.enable();
this.map.keyboard.enable();
if (this.map.tap) this.map.tap.enable();
// Hide loading overlay
const loadingElement = document.getElementById('map-loading');
if (loadingElement) {
loadingElement.style.display = 'none';
console.log('🚫 Loading overlay hidden - hexagons are fully loaded');
}
} }
} }
@ -172,7 +215,7 @@ export default class extends Controller {
const maxPoints = Math.max(...geojsonData.features.map(f => f.properties.point_count)); const maxPoints = Math.max(...geojsonData.features.map(f => f.properties.point_count));
const staticHexagonLayer = L.geoJSON(geojsonData, { const staticHexagonLayer = L.geoJSON(geojsonData, {
style: (feature) => this.styleHexagon(feature, maxPoints), style: (feature) => this.styleHexagon(),
onEachFeature: (feature, layer) => { onEachFeature: (feature, layer) => {
// Add popup with statistics // Add popup with statistics
const props = feature.properties; const props = feature.properties;
@ -190,20 +233,13 @@ export default class extends Controller {
staticHexagonLayer.addTo(this.map); staticHexagonLayer.addTo(this.map);
} }
styleHexagon(feature, maxPoints) { styleHexagon() {
const props = feature.properties;
const pointCount = props.point_count || 0;
// Calculate opacity based on point density
const opacity = 0.2 + (pointCount / maxPoints) * 0.6;
const color = '#3388ff';
return { return {
fillColor: color, fillColor: '#3388ff',
fillOpacity: opacity, fillOpacity: 0.3,
color: color, color: '#3388ff',
weight: 1, weight: 1,
opacity: opacity + 0.2 opacity: 0.3
}; };
} }
@ -213,11 +249,6 @@ export default class extends Controller {
return ` return `
<div style="font-size: 12px; line-height: 1.4;"> <div style="font-size: 12px; line-height: 1.4;">
<h4 style="margin: 0 0 8px 0; color: #2c5aa0;">Hexagon Stats</h4>
<strong>Points:</strong> ${props.point_count || 0}<br>
<strong>Density:</strong> ${props.density || 0} pts/km²<br>
${props.avg_speed ? `<strong>Avg Speed:</strong> ${props.avg_speed} km/h<br>` : ''}
${props.avg_battery ? `<strong>Avg Battery:</strong> ${props.avg_battery}%<br>` : ''}
<strong>Date Range:</strong><br> <strong>Date Range:</strong><br>
<small>${startDate} - ${endDate}</small> <small>${startDate} - ${endDate}</small>
</div> </div>
@ -234,7 +265,7 @@ export default class extends Controller {
opacity: layer.options.opacity opacity: layer.options.opacity
}; };
} }
layer.setStyle({ layer.setStyle({
fillOpacity: 0.8, fillOpacity: 0.8,
weight: 2, weight: 2,
@ -249,6 +280,4 @@ export default class extends Controller {
layer.setStyle(layer._originalStyle); layer.setStyle(layer._originalStyle);
} }
} }
}
// getDataBounds method removed - now using server-provided data bounds
}

View file

@ -316,11 +316,6 @@ export class HexagonGrid {
return ` return `
<div style="font-size: 12px; line-height: 1.4;"> <div style="font-size: 12px; line-height: 1.4;">
<h4 style="margin: 0 0 8px 0; color: #2c5aa0;">Hexagon Stats</h4>
<strong>Points:</strong> ${props.point_count || 0}<br>
<strong>Density:</strong> ${props.density || 0} pts/km²<br>
${props.avg_speed ? `<strong>Avg Speed:</strong> ${props.avg_speed} km/h<br>` : ''}
${props.avg_battery ? `<strong>Avg Battery:</strong> ${props.avg_battery}%<br>` : ''}
<strong>Date Range:</strong><br> <strong>Date Range:</strong><br>
<small>${startDate} - ${endDate}</small> <small>${startDate} - ${endDate}</small>
</div> </div>

View file

@ -1,5 +1,7 @@
<!-- Monthly Digest Header --> <!-- Monthly Digest Header -->
<div class="hero <%= month_gradient_classes(stat) %> text-white rounded-lg shadow-lg mb-8"> <div class="hero text-white rounded-lg shadow-lg mb-8"
style="background-image: url('<%= month_bg_image(stat) %>');">
<div class="hero-overlay bg-opacity-60"></div>
<div class="hero-content text-center relative w-full"> <div class="hero-content text-center relative w-full">
<div class="max-w-md mt-5"> <div class="max-w-md mt-5">
<h1 class="text-4xl font-bold flex items-center justify-center gap-2"> <h1 class="text-4xl font-bold flex items-center justify-center gap-2">

View file

@ -24,14 +24,14 @@
<div class="min-h-screen bg-base-100 mx-auto"> <div class="min-h-screen bg-base-100 mx-auto">
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<!-- Monthly Digest Header --> <!-- Monthly Digest Header -->
<div class="hero <%= month_gradient_classes(@stat) %> text-white rounded-lg shadow-lg mb-8"> <div class="hero text-white rounded-lg shadow-lg mb-8" style="background-image: url('<%= month_bg_image(@stat) %>');">
<div class="hero-content text-center py-6"> <div class="hero-overlay bg-opacity-60"></div>
<div class="max-w-md"> <div class="hero-content text-center py-8">
<div class="max-w-lg">
<h1 class="text-4xl font-bold flex items-center justify-center gap-2"> <h1 class="text-4xl font-bold flex items-center justify-center gap-2">
<%= "#{icon month_icon(@stat)} #{Date::MONTHNAMES[@month]} #{@year}".html_safe %> <%= "#{icon month_icon(@stat)} #{Date::MONTHNAMES[@month]} #{@year}".html_safe %>
</h1> </h1>
<p class="py-6">Monthly Digest (Shared)</p> <p class="pt-6 pb-2">Monthly Digest</p>
<div class="badge badge-warning">Approximate locations only</div>
</div> </div>
</div> </div>
</div> </div>
@ -66,12 +66,7 @@
<!-- Map Summary - Hexagon View --> <!-- Map Summary - Hexagon View -->
<div class="card bg-base-100 shadow-xl mb-8"> <div class="card bg-base-100 shadow-xl mb-8">
<div class="card-body"> <div class="card-body p-0">
<div class="flex justify-between items-center mb-4">
<h2 class="card-title">🗺️ Activity Overview</h2>
<div class="badge badge-warning">Privacy-safe view</div>
</div>
<!-- Hexagon Map Container --> <!-- Hexagon Map Container -->
<div class="w-full h-96 rounded-lg border border-base-300 relative overflow-hidden"> <div class="w-full h-96 rounded-lg border border-base-300 relative overflow-hidden">
<div id="public-monthly-stats-map" class="w-full h-full" <div id="public-monthly-stats-map" class="w-full h-full"
@ -82,13 +77,11 @@
data-public-stat-map-data-bounds-value="<%= @data_bounds.to_json if @data_bounds %>"></div> data-public-stat-map-data-bounds-value="<%= @data_bounds.to_json if @data_bounds %>"></div>
<!-- Loading overlay --> <!-- Loading overlay -->
<div id="map-loading" class="absolute inset-0 bg-base-200 flex items-center justify-center"> <div id="map-loading" class="absolute inset-0 bg-base-200 bg-opacity-80 flex items-center justify-center z-50">
<span class="loading loading-spinner loading-lg text-primary"></span> <div class="text-center">
</div> <span class="loading loading-spinner loading-lg text-primary"></span>
<p class="text-sm mt-2 text-base-content">Loading hexagons...</p>
<!-- Privacy overlay --> </div>
<div class="absolute top-2 left-2 bg-warning text-warning-content text-xs px-2 py-1 rounded">
🔒 Approximate locations only
</div> </div>
</div> </div>
</div> </div>

View file

@ -2,9 +2,7 @@
class RecalculateTripsDistance < ActiveRecord::Migration[8.0] class RecalculateTripsDistance < ActiveRecord::Migration[8.0]
def up def up
Trip.find_each do |trip| Trip.find_each(&:enqueue_calculation_jobs)
trip.enqueue_calculation_jobs
end
end end
def down def down