mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
Add flags and chart to email
This commit is contained in:
parent
da9e440cfa
commit
d7016e57b4
14 changed files with 60 additions and 159 deletions
1
Gemfile
1
Gemfile
|
|
@ -18,7 +18,6 @@ gem 'foreman'
|
|||
gem 'geocoder', github: 'Freika/geocoder', branch: 'master'
|
||||
gem 'gpx'
|
||||
gem 'groupdate'
|
||||
gem 'grover'
|
||||
gem 'h3', '~> 3.7'
|
||||
gem 'httparty'
|
||||
gem 'importmap-rails'
|
||||
|
|
|
|||
|
|
@ -204,8 +204,6 @@ GEM
|
|||
rake
|
||||
groupdate (6.7.0)
|
||||
activesupport (>= 7.1)
|
||||
grover (1.2.4)
|
||||
nokogiri (~> 1)
|
||||
h3 (3.7.4)
|
||||
ffi (~> 1.9)
|
||||
rgeo-geojson (~> 2.1)
|
||||
|
|
@ -659,7 +657,6 @@ DEPENDENCIES
|
|||
geocoder!
|
||||
gpx
|
||||
groupdate
|
||||
grover
|
||||
h3 (~> 3.7)
|
||||
httparty
|
||||
importmap-rails
|
||||
|
|
|
|||
1
app/assets/svg/icons/lucide/outline/mail.svg
Normal file
1
app/assets/svg/icons/lucide/outline/mail.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail-icon lucide-mail"><path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"/><rect x="2" y="4" width="20" height="16" rx="2"/></svg>
|
||||
|
After Width: | Height: | Size: 332 B |
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class Shared::DigestsController < ApplicationController
|
||||
helper Users::DigestsHelper
|
||||
helper CountryFlagHelper
|
||||
|
||||
before_action :authenticate_user!, except: [:show]
|
||||
before_action :authenticate_active_user!, only: [:update]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class Users::DigestsController < ApplicationController
|
||||
helper Users::DigestsHelper
|
||||
helper CountryFlagHelper
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :authenticate_active_user!, only: [:create]
|
||||
|
|
|
|||
|
|
@ -2,35 +2,16 @@
|
|||
|
||||
class Users::DigestsMailer < ApplicationMailer
|
||||
helper Users::DigestsHelper
|
||||
helper CountryFlagHelper
|
||||
|
||||
def year_end_digest
|
||||
@user = params[:user]
|
||||
@digest = params[:digest]
|
||||
@distance_unit = @user.safe_settings.distance_unit || 'km'
|
||||
|
||||
# Generate chart image
|
||||
@chart_image_name = generate_chart_attachment
|
||||
|
||||
mail(
|
||||
to: @user.email,
|
||||
subject: "Your #{@digest.year} Year in Review - Dawarich"
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_chart_attachment
|
||||
image_data = Users::Digests::ChartImageGenerator.new(@digest, distance_unit: @distance_unit).call
|
||||
filename = 'monthly_distance_chart.png'
|
||||
|
||||
attachments.inline[filename] = {
|
||||
mime_type: 'image/png',
|
||||
content: image_data
|
||||
}
|
||||
|
||||
filename
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Failed to generate chart image: #{e.message}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Users
|
||||
module Digests
|
||||
class ChartImageGenerator
|
||||
def initialize(digest, distance_unit: 'km')
|
||||
@digest = digest
|
||||
@distance_unit = distance_unit
|
||||
end
|
||||
|
||||
def call
|
||||
html = render_chart_html
|
||||
generate_image(html)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :digest, :distance_unit
|
||||
|
||||
def render_chart_html
|
||||
ApplicationController.render(
|
||||
template: 'user/digests/chart',
|
||||
layout: false,
|
||||
assigns: {
|
||||
monthly_distances: digest.monthly_distances,
|
||||
distance_unit: distance_unit
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def generate_image(html)
|
||||
grover = Grover.new(
|
||||
html,
|
||||
format: 'png',
|
||||
viewport: { width: 600, height: 320 },
|
||||
wait_until: 'networkidle0'
|
||||
)
|
||||
grover.to_png
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -141,7 +141,9 @@ class Users::SafeSettings
|
|||
settings['maps_maplibre_style']
|
||||
end
|
||||
|
||||
def digest_emails_enabled?
|
||||
def digest_emails_enabled
|
||||
settings['digest_emails_enabled'] != false
|
||||
end
|
||||
|
||||
alias digest_emails_enabled? digest_emails_enabled
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
#chart-container {
|
||||
width: 560px;
|
||||
height: 280px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="chart-container">
|
||||
<canvas id="monthlyChart"></canvas>
|
||||
</div>
|
||||
<script>
|
||||
const ctx = document.getElementById('monthlyChart').getContext('2d');
|
||||
const rawData = <%= @monthly_distances.to_json.html_safe %>;
|
||||
|
||||
// Convert to km and ensure all 12 months are present
|
||||
const monthlyData = [];
|
||||
for (let i = 1; i <= 12; i++) {
|
||||
const meters = rawData[i.toString()] || 0;
|
||||
monthlyData.push((meters / 1000).toFixed(1));
|
||||
}
|
||||
|
||||
const monthColors = [
|
||||
'#397bb5', '#5A4E9D', '#3B945E', '#7BC96F',
|
||||
'#FFD54F', '#FFA94D', '#FF6B6B', '#FF8C42',
|
||||
'#C97E4F', '#8B4513', '#5A2E2E', '#265d7d'
|
||||
];
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
datasets: [{
|
||||
label: 'Distance (<%= @distance_unit %>)',
|
||||
data: monthlyData,
|
||||
backgroundColor: monthColors,
|
||||
borderRadius: 4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Your Year, Month by Month',
|
||||
font: { size: 16, weight: 'bold' }
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: '<%= @distance_unit %>'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -108,7 +108,10 @@
|
|||
<ul class="space-y-2 w-full">
|
||||
<% @digest.top_countries_by_time.take(3).each do |country| %>
|
||||
<li class="flex justify-between items-center p-3 bg-base-200 rounded-lg">
|
||||
<span class="font-semibold"><%= country['name'] %></span>
|
||||
<span class="font-semibold">
|
||||
<span class="mr-1"><%= country_flag(country['name']) %></span>
|
||||
<%= country['name'] %>
|
||||
</span>
|
||||
<span class="text-gray-600"><%= format_time_spent(country['minutes']) %></span>
|
||||
</li>
|
||||
<% end %>
|
||||
|
|
@ -127,7 +130,10 @@
|
|||
<% @digest.toponyms&.each_with_index do |country, index| %>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-semibold"><%= country['country'] %></span>
|
||||
<span class="font-semibold">
|
||||
<span class="mr-1"><%= country_flag(country['country']) %></span>
|
||||
<%= country['country'] %>
|
||||
</span>
|
||||
<span class="text-sm"><%= country['cities']&.length || 0 %> cities</span>
|
||||
</div>
|
||||
<progress class="progress progress-primary w-full" value="<%= 100 - (index * 15) %>" max="100"></progress>
|
||||
|
|
|
|||
|
|
@ -134,7 +134,10 @@
|
|||
<span class="badge badge-lg <%= ['badge-primary', 'badge-secondary', 'badge-accent', 'badge-info', 'badge-success'][index] %>">
|
||||
<%= index + 1 %>
|
||||
</span>
|
||||
<span class="font-semibold"><%= country['name'] %></span>
|
||||
<span class="font-semibold">
|
||||
<span class="mr-1"><%= country_flag(country['name']) %></span>
|
||||
<%= country['name'] %>
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-gray-600"><%= format_time_spent(country['minutes']) %></span>
|
||||
</div>
|
||||
|
|
@ -162,7 +165,10 @@
|
|||
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-semibold"><%= country['country'] %></span>
|
||||
<span class="font-semibold">
|
||||
<span class="mr-1"><%= country_flag(country['country']) %></span>
|
||||
<%= country['country'] %>
|
||||
</span>
|
||||
<span class="text-sm">
|
||||
<%= pluralize(cities_count, 'city') %>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -208,10 +208,34 @@
|
|||
</div>
|
||||
|
||||
<!-- Monthly Distance Chart -->
|
||||
<% if @chart_image_name %>
|
||||
<% if @digest.monthly_distances.present? %>
|
||||
<div class="chart-container">
|
||||
<h3 style="margin: 0 0 16px 0; color: #1e293b;">Your Year, Month by Month</h3>
|
||||
<%= image_tag attachments[@chart_image_name].url, alt: 'Monthly Distance Chart' %>
|
||||
<% max_distance = @digest.monthly_distances.values.map(&:to_i).max %>
|
||||
<% max_distance = 1 if max_distance.zero? %>
|
||||
<% chart_height = 120 %>
|
||||
<% bar_colors = ['#3b82f6', '#6366f1', '#8b5cf6', '#a855f7', '#d946ef', '#ec4899', '#f43f5e', '#ef4444', '#f97316', '#eab308', '#84cc16', '#22c55e'] %>
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse: collapse;">
|
||||
<!-- Bars row -->
|
||||
<tr>
|
||||
<% (1..12).each do |month| %>
|
||||
<% distance = @digest.monthly_distances[month.to_s].to_i %>
|
||||
<% bar_height = (distance.to_f / max_distance * chart_height).round %>
|
||||
<% bar_height = 3 if bar_height < 3 && distance > 0 %>
|
||||
<td style="vertical-align: bottom; text-align: center; padding: 0 2px;">
|
||||
<div style="background: <%= bar_colors[month - 1] %>; width: 100%; height: <%= bar_height %>px; border-radius: 3px 3px 0 0; min-height: 3px;"></div>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<!-- Labels row -->
|
||||
<tr>
|
||||
<% (1..12).each do |month| %>
|
||||
<td style="text-align: center; padding-top: 6px; font-size: 11px; color: #64748b;">
|
||||
<%= Date::ABBR_MONTHNAMES[month][0..0] %>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
|
@ -222,7 +246,7 @@
|
|||
<ul class="location-list">
|
||||
<% @digest.top_countries_by_time.take(3).each do |country| %>
|
||||
<li>
|
||||
<span><%= country['name'] %></span>
|
||||
<span><%= country_flag(country['name']) %> <%= country['name'] %></span>
|
||||
<span><%= format_time_spent(country['minutes']) %></span>
|
||||
</li>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Grover.configure do |config|
|
||||
config.options = {
|
||||
format: 'png',
|
||||
quality: 90,
|
||||
wait_until: 'networkidle0',
|
||||
launch_args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage']
|
||||
}
|
||||
end
|
||||
10
spec/mailers/previews/users/digests_mailer_preview.rb
Normal file
10
spec/mailers/previews/users/digests_mailer_preview.rb
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Users::DigestsMailerPreview < ActionMailer::Preview
|
||||
def year_end_digest
|
||||
user = User.first
|
||||
digest = user.digests.yearly.last || Users::Digest.last
|
||||
|
||||
Users::DigestsMailer.with(user: user, digest: digest).year_end_digest
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue