ruby_cms 0.2.0.5 → 0.2.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -1
  3. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +9 -1
  4. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +3 -3
  5. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +8 -8
  6. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +15 -5
  7. data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +57 -12
  8. data/app/controllers/concerns/ruby_cms/page_tracking.rb +15 -0
  9. data/app/controllers/ruby_cms/admin/analytics_controller.rb +12 -0
  10. data/app/controllers/ruby_cms/admin/commands_controller.rb +6 -6
  11. data/app/javascript/controllers/ruby_cms/bulk_action_table_controller.js +65 -13
  12. data/app/services/ruby_cms/analytics/report.rb +119 -7
  13. data/app/services/ruby_cms/command_runner.rb +3 -3
  14. data/app/views/ruby_cms/admin/analytics/index.html.erb +160 -67
  15. data/app/views/ruby_cms/admin/analytics/partials/_browser_device.html.erb +21 -15
  16. data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +36 -16
  17. data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +29 -13
  18. data/app/views/ruby_cms/admin/analytics/partials/_landing_pages.html.erb +27 -15
  19. data/app/views/ruby_cms/admin/analytics/partials/_os_stats.html.erb +29 -18
  20. data/app/views/ruby_cms/admin/analytics/partials/_popular_pages.html.erb +17 -12
  21. data/app/views/ruby_cms/admin/analytics/partials/_recent_activity.html.erb +25 -14
  22. data/app/views/ruby_cms/admin/analytics/partials/_top_referrers.html.erb +27 -15
  23. data/app/views/ruby_cms/admin/analytics/partials/_top_visitors.html.erb +20 -10
  24. data/app/views/ruby_cms/admin/analytics/partials/_utm_sources.html.erb +27 -15
  25. data/app/views/ruby_cms/admin/content_blocks/index.html.erb +8 -1
  26. data/config/locales/en.yml +69 -5
  27. data/config/locales/nl.yml +98 -0
  28. data/lib/generators/ruby_cms/install_generator.rb +21 -6
  29. data/lib/generators/ruby_cms/templates/ruby_cms.rb +40 -3
  30. data/lib/ruby_cms/settings_registry.rb +35 -0
  31. data/lib/ruby_cms/version.rb +1 -1
  32. metadata +1 -1
@@ -1,16 +1,21 @@
1
- <div class="rounded-lg border border-border/60 bg-white shadow-sm">
2
- <div class="flex items-center justify-between gap-3 px-4 py-2 border-b border-border/40">
3
- <div>
4
- <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.daily_activity", default: "Daily Activity") %></p>
5
- <p class="text-sm text-muted-foreground"><%= t("ruby_cms.admin.analytics.page_views_visitors_over_time", default: "Page views and visitors over time") %></p>
1
+ <div class="rounded-xl border border-border/60 bg-white shadow-sm overflow-hidden">
2
+ <div class="flex items-center justify-between gap-3 px-4 py-3 border-b border-border/40">
3
+ <div class="flex items-center gap-3">
4
+ <div class="rounded-md bg-sky-50 p-1.5 flex-shrink-0">
5
+ <svg class="w-4 h-4 text-sky-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"/></svg>
6
+ </div>
7
+ <div>
8
+ <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.daily_activity", default: "Daily Activity") %></p>
9
+ <p class="text-xs text-muted-foreground"><%= t("ruby_cms.admin.analytics.page_views_visitors_over_time", default: "Page views and visitors over time") %></p>
10
+ </div>
6
11
  </div>
7
- <div class="flex items-center gap-4 text-sm text-muted-foreground">
12
+ <div class="flex items-center gap-4 text-xs text-muted-foreground flex-shrink-0">
8
13
  <span class="inline-flex items-center gap-1.5"><span class="h-2 w-2 rounded-full bg-primary"></span><%= t("ruby_cms.admin.analytics.views", default: "Views") %></span>
9
14
  <span class="inline-flex items-center gap-1.5"><span class="h-2 w-2 rounded-full bg-emerald-500"></span><%= t("ruby_cms.admin.analytics.visitors", default: "Visitors") %></span>
10
15
  </div>
11
16
  </div>
12
17
 
13
- <div class="px-4 py-3">
18
+ <div class="px-4 py-4">
14
19
  <% if @daily_activity.present? && @daily_activity.any? %>
15
20
  <%
16
21
  daily_arr = @daily_activity.is_a?(Hash) ? @daily_activity.to_a : @daily_activity
@@ -18,11 +23,11 @@
18
23
  max_visitors = (@daily_visitors || {}).values.map(&:to_i).max || 0
19
24
  max_value = [max_views, max_visitors].max
20
25
  max_value = 1 if max_value.zero?
21
- chart_h = 130
26
+ chart_h = 160
22
27
  %>
23
28
 
24
29
  <div class="grid grid-cols-[2.75rem_1fr] gap-2">
25
- <div class="flex flex-col justify-between h-[<%= chart_h %>px] text-xs text-muted-foreground tabular-nums">
30
+ <div class="flex flex-col justify-between text-xs text-muted-foreground tabular-nums" style="height:<%= chart_h %>px">
26
31
  <% [100, 75, 50, 25, 0].each do |pct| %>
27
32
  <div class="text-right"><%= number_with_delimiter((max_value * pct / 100.0).round) %></div>
28
33
  <% end %>
@@ -30,18 +35,32 @@
30
35
 
31
36
  <div class="relative">
32
37
  <div class="absolute inset-0 flex flex-col justify-between pointer-events-none">
33
- <% 5.times do %><div class="h-px bg-border/40"></div><% end %>
38
+ <% 5.times do %><div class="h-px bg-border/30"></div><% end %>
34
39
  </div>
35
40
 
36
- <div class="h-[<%= chart_h %>px] flex items-end gap-1">
41
+ <div class="flex items-end gap-1" style="height:<%= chart_h %>px">
37
42
  <% daily_arr.each do |date, views_count| %>
38
43
  <% visitors_count = (@daily_visitors || {})[date] || 0 %>
39
- <% views_h = [((views_count.to_f / max_value) * chart_h).round, 2].max %>
40
- <% visitors_h = [((visitors_count.to_f / max_value) * chart_h).round, 2].max %>
44
+ <% views_h = if views_count.to_i.positive?
45
+ [((views_count.to_f / max_value) * chart_h).round, 2].max
46
+ else
47
+ 0
48
+ end %>
49
+ <% visitors_h = if visitors_count.to_i.positive?
50
+ [((visitors_count.to_f / max_value) * chart_h).round, 2].max
51
+ else
52
+ 0
53
+ end %>
41
54
  <div class="flex flex-col items-center gap-1.5 flex-1 min-w-0 group">
42
55
  <div class="w-full flex items-end justify-center gap-px" title="<%= format_chart_date(date) %> — Views: <%= number_with_delimiter(views_count) %>, Visitors: <%= number_with_delimiter(visitors_count) %>">
43
- <div class="flex-1 max-w-[8px] rounded-t bg-primary/80 group-hover:bg-primary transition-colors" style="height:<%= views_h %>px"></div>
44
- <div class="flex-1 max-w-[8px] rounded-t bg-emerald-500/80 group-hover:bg-emerald-500 transition-colors" style="height:<%= visitors_h %>px"></div>
56
+ <div
57
+ class="flex-1 rounded-t transition-colors group-hover:opacity-90"
58
+ style="height:<%= views_h %>px; max-width:8px; background-color:hsl(var(--primary) / 0.75);"
59
+ ></div>
60
+ <div
61
+ class="flex-1 rounded-t transition-colors group-hover:opacity-90"
62
+ style="height:<%= visitors_h %>px; max-width:8px; background-color:rgb(16 185 129 / 0.75);"
63
+ ></div>
45
64
  </div>
46
65
  <div class="text-[10px] text-muted-foreground truncate w-full text-center"><%= format_chart_date_short(date) %></div>
47
66
  </div>
@@ -50,7 +69,8 @@
50
69
  </div>
51
70
  </div>
52
71
  <% else %>
53
- <div class="flex items-center justify-center h-40">
72
+ <div class="flex flex-col items-center justify-center h-44 gap-2">
73
+ <svg class="w-8 h-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"/></svg>
54
74
  <p class="text-sm text-muted-foreground"><%= t("ruby_cms.admin.analytics.no_daily_data", default: "No daily activity data") %></p>
55
75
  </div>
56
76
  <% end %>
@@ -1,10 +1,15 @@
1
- <div class="rounded-lg border border-border/60 bg-white shadow-sm">
2
- <div class="px-4 py-2 border-b border-border/40">
3
- <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.hourly_activity", default: "Hourly Activity") %></p>
4
- <p class="text-sm text-muted-foreground"><%= t("ruby_cms.admin.analytics.views_by_hour", default: "Views by hour") %></p>
1
+ <div class="rounded-xl border border-border/60 bg-white shadow-sm overflow-hidden">
2
+ <div class="flex items-center gap-3 px-4 py-3 border-b border-border/40">
3
+ <div class="rounded-md bg-emerald-50 p-1.5 flex-shrink-0">
4
+ <svg class="w-4 h-4 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
5
+ </div>
6
+ <div>
7
+ <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.hourly_activity", default: "Hourly Activity") %></p>
8
+ <p class="text-xs text-muted-foreground"><%= t("ruby_cms.admin.analytics.views_by_hour", default: "Views by hour of day") %></p>
9
+ </div>
5
10
  </div>
6
11
 
7
- <div class="px-4 py-3">
12
+ <div class="px-4 py-4">
8
13
  <% if @hourly_activity.present? && @hourly_activity.any? %>
9
14
  <%
10
15
  hourly_arr = @hourly_activity.is_a?(Hash) ? @hourly_activity.to_a : @hourly_activity
@@ -13,12 +18,13 @@
13
18
  total = group.sum { |_, c| c.to_i }
14
19
  [hours, total]
15
20
  end
16
- max_value = grouped.map { |_, c| c }.max || 1
17
- chart_h = 130
21
+ max_value = grouped.map { |_, c| c.to_i }.max.to_f
22
+ max_value = 1.0 unless max_value.positive? && max_value.finite?
23
+ chart_h = 160
18
24
  %>
19
25
 
20
26
  <div class="grid grid-cols-[2.75rem_1fr] gap-2">
21
- <div class="flex flex-col justify-between h-[<%= chart_h %>px] text-xs text-muted-foreground tabular-nums">
27
+ <div class="flex flex-col justify-between text-xs text-muted-foreground tabular-nums" style="height:<%= chart_h %>px">
22
28
  <% [100, 75, 50, 25, 0].each do |pct| %>
23
29
  <div class="text-right"><%= number_with_delimiter((max_value * pct / 100.0).round) %></div>
24
30
  <% end %>
@@ -26,15 +32,24 @@
26
32
 
27
33
  <div class="relative">
28
34
  <div class="absolute inset-0 flex flex-col justify-between pointer-events-none">
29
- <% 5.times do %><div class="h-px bg-border/40"></div><% end %>
35
+ <% 5.times do %><div class="h-px bg-border/30"></div><% end %>
30
36
  </div>
31
37
 
32
- <div class="h-[<%= chart_h %>px] flex items-end gap-1.5">
38
+ <div class="flex items-end gap-1.5" style="height:<%= chart_h %>px">
33
39
  <% grouped.each do |hours, count| %>
34
- <% h = [((count.to_f / max_value) * chart_h).round, 2].max %>
40
+ <% ratio = count.to_f / max_value %>
41
+ <% ratio = 0.0 unless ratio.finite? %>
42
+ <% h = if count.to_i.positive?
43
+ [[(ratio * chart_h).round, 2].max, chart_h].min
44
+ else
45
+ 0
46
+ end %>
35
47
  <div class="flex flex-col items-center gap-1.5 flex-1 min-w-0 group">
36
48
  <div class="w-full flex items-end justify-center" title="<%= hours.size > 1 ? "#{hours.first}:00–#{hours.last}:00" : "#{hours.first}:00" %> — <%= number_with_delimiter(count) %> views">
37
- <div class="w-full max-w-[12px] rounded-t bg-emerald-500/80 group-hover:bg-emerald-500 transition-colors" style="height:<%= h %>px"></div>
49
+ <div
50
+ class="w-full rounded-t transition-colors group-hover:opacity-90"
51
+ style="height:<%= h %>px; max-width:12px; background-color:rgb(16 185 129 / 0.75);"
52
+ ></div>
38
53
  </div>
39
54
  <div class="text-[10px] text-muted-foreground truncate w-full text-center"><%= hours.size > 1 ? "#{hours.first}–#{hours.last}h" : "#{hours.first}h" %></div>
40
55
  </div>
@@ -43,7 +58,8 @@
43
58
  </div>
44
59
  </div>
45
60
  <% else %>
46
- <div class="flex items-center justify-center h-40">
61
+ <div class="flex flex-col items-center justify-center h-44 gap-2">
62
+ <svg class="w-8 h-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
47
63
  <p class="text-sm text-muted-foreground"><%= t("ruby_cms.admin.analytics.no_hourly_data", default: "No hourly activity data") %></p>
48
64
  </div>
49
65
  <% end %>
@@ -1,21 +1,33 @@
1
- <div class="rounded-lg border border-border/60 bg-white shadow-sm">
2
- <div class="px-6 py-4 border-b border-border/40">
3
- <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.landing_pages", default: "Landing Pages") %></p>
4
- <p class="text-sm text-muted-foreground">First pages visitors see.</p>
1
+ <div class="rounded-xl border border-border/60 bg-white shadow-sm overflow-hidden">
2
+ <div class="flex items-center gap-3 px-4 py-3 border-b border-border/40">
3
+ <div class="rounded-md bg-teal-50 p-1.5 flex-shrink-0">
4
+ <svg class="w-4 h-4 text-teal-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
5
+ </div>
6
+ <div>
7
+ <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.landing_pages", default: "Landing Pages") %></p>
8
+ <p class="text-xs text-muted-foreground">First pages visitors see</p>
9
+ </div>
5
10
  </div>
6
11
 
7
- <div class="divide-y divide-border/40">
8
- <% if @landing_pages.present? && @landing_pages.any? %>
12
+ <% if @landing_pages.present? && @landing_pages.any? %>
13
+ <% total = @landing_pages.values.sum.to_f %>
14
+ <div class="divide-y divide-border/30">
9
15
  <% @landing_pages.first(6).each do |page, count| %>
10
- <div class="flex items-center justify-between gap-3 px-6 py-3">
11
- <span class="text-sm text-foreground truncate" title="<%= page %>"><%= truncate(page.to_s.gsub(%r{https?://[^/]+}, ''), length: 40) %></span>
12
- <span class="text-sm font-semibold tabular-nums text-foreground flex-shrink-0"><%= number_with_delimiter(count) %></span>
16
+ <% pct = total.positive? ? (count / total * 100).round(1) : 0 %>
17
+ <div class="relative flex items-center justify-between gap-3 px-4 py-2.5">
18
+ <div class="absolute inset-y-0 left-0 bg-teal-500/5" style="width: <%= pct %>%"></div>
19
+ <span class="relative z-10 text-sm text-foreground truncate" title="<%= page %>"><%= truncate(page.to_s.gsub(%r{https?://[^/]+}, ''), length: 38) %></span>
20
+ <div class="relative z-10 flex items-center gap-2 flex-shrink-0">
21
+ <span class="text-xs text-muted-foreground tabular-nums"><%= pct %>%</span>
22
+ <span class="text-sm font-semibold tabular-nums text-foreground"><%= number_with_delimiter(count) %></span>
23
+ </div>
13
24
  </div>
14
25
  <% end %>
15
- <% else %>
16
- <div class="px-6 py-10 text-center">
17
- <p class="text-sm text-muted-foreground">No landing page data</p>
18
- </div>
19
- <% end %>
20
- </div>
26
+ </div>
27
+ <% else %>
28
+ <div class="flex flex-col items-center justify-center px-4 py-10 gap-2">
29
+ <svg class="w-8 h-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
30
+ <p class="text-sm text-muted-foreground">No landing page data</p>
31
+ </div>
32
+ <% end %>
21
33
  </div>
@@ -1,26 +1,37 @@
1
- <div class="rounded-lg border border-border/60 bg-white shadow-sm">
2
- <div class="px-6 py-4 border-b border-border/40">
3
- <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.operating_systems", default: "Operating Systems") %></p>
4
- <p class="text-sm text-muted-foreground">Visitor OS breakdown.</p>
1
+ <div class="rounded-xl border border-border/60 bg-white shadow-sm overflow-hidden">
2
+ <div class="flex items-center gap-3 px-4 py-3 border-b border-border/40">
3
+ <div class="rounded-md bg-indigo-50 p-1.5 flex-shrink-0">
4
+ <svg class="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2V9M9 21H5a2 2 0 01-2-2V9m0 0h18"/></svg>
5
+ </div>
6
+ <div>
7
+ <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.operating_systems", default: "Operating Systems") %></p>
8
+ <p class="text-xs text-muted-foreground">Visitor OS breakdown</p>
9
+ </div>
5
10
  </div>
6
11
 
7
- <div class="divide-y divide-border/40">
8
- <% if @os_stats.present? && @os_stats.any? %>
9
- <% total = @os_stats.values.sum.to_f %>
12
+ <% if @os_stats.present? && @os_stats.any? %>
13
+ <% total = @os_stats.values.sum.to_f %>
14
+ <div class="px-4 py-3 space-y-2.5">
10
15
  <% @os_stats.sort_by { |_, v| -v }.first(6).each do |os, count| %>
11
16
  <% pct = total.positive? ? (count / total * 100).round(1) : 0 %>
12
- <div class="flex items-center justify-between gap-3 px-6 py-3">
13
- <span class="text-sm text-foreground"><%= os %></span>
14
- <div class="flex items-center gap-3 flex-shrink-0">
15
- <span class="text-xs text-muted-foreground tabular-nums"><%= pct %>%</span>
16
- <span class="text-sm font-semibold tabular-nums text-foreground"><%= number_with_delimiter(count) %></span>
17
+ <div>
18
+ <div class="flex items-center justify-between gap-3 mb-1">
19
+ <span class="text-sm text-foreground"><%= os %></span>
20
+ <div class="flex items-center gap-2 flex-shrink-0">
21
+ <span class="text-xs text-muted-foreground tabular-nums"><%= pct %>%</span>
22
+ <span class="text-sm font-semibold tabular-nums text-foreground"><%= number_with_delimiter(count) %></span>
23
+ </div>
24
+ </div>
25
+ <div class="w-full h-1.5 bg-muted rounded-full overflow-hidden">
26
+ <div class="h-full bg-indigo-400 rounded-full" style="width: <%= pct %>%"></div>
17
27
  </div>
18
28
  </div>
19
29
  <% end %>
20
- <% else %>
21
- <div class="px-6 py-10 text-center">
22
- <p class="text-sm text-muted-foreground"><%= t("ruby_cms.admin.analytics.no_data", default: "No data yet.") %></p>
23
- </div>
24
- <% end %>
25
- </div>
30
+ </div>
31
+ <% else %>
32
+ <div class="flex flex-col items-center justify-center px-4 py-10 gap-2">
33
+ <svg class="w-8 h-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2V9M9 21H5a2 2 0 01-2-2V9m0 0h18"/></svg>
34
+ <p class="text-sm text-muted-foreground"><%= t("ruby_cms.admin.analytics.no_data", default: "No data yet.") %></p>
35
+ </div>
36
+ <% end %>
26
37
  </div>
@@ -1,34 +1,39 @@
1
- <div class="rounded-lg border border-border/60 bg-white shadow-sm">
2
- <div class="flex items-center justify-between gap-3 px-4 py-3 border-b border-border/40">
1
+ <div class="rounded-xl border border-border/60 bg-white shadow-sm overflow-hidden">
2
+ <div class="flex items-center gap-3 px-4 py-3 border-b border-border/40">
3
+ <div class="rounded-md bg-blue-50 p-1.5 flex-shrink-0">
4
+ <svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
5
+ </div>
3
6
  <div>
4
7
  <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.popular_pages", default: "Popular Pages") %></p>
5
- <p class="text-sm text-muted-foreground">Most viewed pages in selected range.</p>
8
+ <p class="text-xs text-muted-foreground">Most viewed pages in selected range</p>
6
9
  </div>
7
10
  </div>
8
11
 
9
12
  <% if @popular_pages.present? %>
10
13
  <% total = @popular_pages.values.sum.to_f %>
11
- <div class="divide-y divide-border/40">
14
+ <div class="divide-y divide-border/30">
12
15
  <% @popular_pages.first(8).each_with_index do |(page_name, count), idx| %>
13
16
  <% pct = total.positive? ? (count / total * 100).round(1) : 0 %>
14
17
  <%= link_to page_details_ruby_cms_admin_analytics_path(page_name:, start_date: @start_date, end_date: @end_date, period: @period),
15
- class: "group relative flex items-center justify-between gap-2 px-4 py-2 hover:bg-muted/50 transition-colors" do %>
16
- <div class="absolute inset-y-0 left-0 bg-primary/5 transition-all" style="width: <%= pct %>%"></div>
17
- <div class="relative z-10 flex items-center gap-2 min-w-0">
18
+ class: "group relative flex items-center justify-between gap-3 px-4 py-2.5 hover:bg-muted/40 transition-colors" do %>
19
+ <div class="absolute inset-y-0 left-0 bg-blue-500/5 transition-all" style="width: <%= pct %>%"></div>
20
+ <div class="relative z-10 flex items-center gap-3 min-w-0">
18
21
  <span class="text-xs font-medium text-muted-foreground tabular-nums w-5 text-right flex-shrink-0"><%= idx + 1 %></span>
19
- <p class="text-xs font-medium text-foreground truncate group-hover:text-primary"><%= page_name.to_s.humanize %></p>
22
+ <p class="text-sm text-foreground truncate group-hover:text-primary transition-colors"><%= page_name.to_s.humanize %></p>
20
23
  </div>
21
- <div class="relative z-10 flex items-center gap-2 flex-shrink-0">
24
+ <div class="relative z-10 flex items-center gap-2.5 flex-shrink-0">
22
25
  <span class="text-xs text-muted-foreground tabular-nums"><%= pct %>%</span>
23
- <span class="text-xs font-semibold tabular-nums text-foreground"><%= number_with_delimiter(count) %></span>
26
+ <span class="text-sm font-semibold tabular-nums text-foreground"><%= number_with_delimiter(count) %></span>
27
+ <svg class="w-3.5 h-3.5 text-muted-foreground/40 group-hover:text-primary/60 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
24
28
  </div>
25
29
  <% end %>
26
30
  <% end %>
27
31
  </div>
28
32
  <% else %>
29
- <div class="px-4 py-8 text-center">
33
+ <div class="flex flex-col items-center justify-center px-4 py-10 gap-2">
34
+ <svg class="w-8 h-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
30
35
  <p class="text-sm font-medium text-foreground"><%= t("ruby_cms.admin.analytics.no_data", default: "No data yet.") %></p>
31
- <p class="mt-1 text-sm text-muted-foreground">Track some traffic to see page analytics.</p>
36
+ <p class="text-xs text-muted-foreground">Track some traffic to see page analytics</p>
32
37
  </div>
33
38
  <% end %>
34
39
  </div>
@@ -1,33 +1,44 @@
1
- <div class="rounded-lg border border-border/60 bg-white shadow-sm">
2
- <div class="px-4 py-3 border-b border-border/40">
3
- <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.recent_activity", default: "Recent Activity") %></p>
4
- <p class="text-sm text-muted-foreground">Latest page views.</p>
1
+ <div class="rounded-xl border border-border/60 bg-white shadow-sm overflow-hidden">
2
+ <div class="flex items-center gap-3 px-4 py-3 border-b border-border/40">
3
+ <div class="rounded-md bg-green-50 p-1.5 flex-shrink-0">
4
+ <svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
5
+ </div>
6
+ <div>
7
+ <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.recent_activity", default: "Recent Activity") %></p>
8
+ <p class="text-xs text-muted-foreground">Latest page views</p>
9
+ </div>
5
10
  </div>
6
11
 
7
12
  <% if @recent_page_views.present? && @recent_page_views.any? %>
8
- <div class="divide-y divide-border/40">
13
+ <div class="divide-y divide-border/30">
9
14
  <% @recent_page_views.first(6).each do |event| %>
10
15
  <% page_name = event.respond_to?(:page_name) ? event.page_name : nil %>
11
16
  <% if page_name.present? %>
12
17
  <%= link_to page_details_ruby_cms_admin_analytics_path(page_name: page_name, start_date: @start_date, end_date: @end_date, period: @period),
13
- class: "group flex items-center justify-between gap-2 px-4 py-2 hover:bg-muted/50 transition-colors" do %>
14
- <div class="min-w-0">
15
- <p class="text-xs font-medium text-foreground truncate group-hover:text-primary"><%= page_name.to_s.humanize %></p>
16
- <p class="text-[10px] text-muted-foreground mt-0.5"><%= time_ago_in_words(event.time) %> ago</p>
18
+ class: "group flex items-center gap-3 px-4 py-2.5 hover:bg-muted/40 transition-colors" do %>
19
+ <div class="w-1.5 h-1.5 rounded-full bg-green-400 flex-shrink-0 mt-0.5"></div>
20
+ <div class="min-w-0 flex-1">
21
+ <p class="text-sm text-foreground truncate group-hover:text-primary transition-colors"><%= page_name.to_s.humanize %></p>
22
+ <p class="text-xs text-muted-foreground mt-0.5"><%= time_ago_in_words(event.time) %> ago</p>
17
23
  </div>
24
+ <svg class="w-3.5 h-3.5 text-muted-foreground/40 group-hover:text-primary/60 transition-colors flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
18
25
  <% end %>
19
26
  <% else %>
20
- <div class="px-4 py-2">
21
- <p class="text-xs font-medium text-foreground"><%= t("ruby_cms.admin.analytics.page", default: "Page") %></p>
22
- <p class="text-[10px] text-muted-foreground mt-0.5"><%= time_ago_in_words(event.time) %> ago</p>
27
+ <div class="flex items-center gap-3 px-4 py-2.5">
28
+ <div class="w-1.5 h-1.5 rounded-full bg-muted-foreground/30 flex-shrink-0 mt-0.5"></div>
29
+ <div class="min-w-0">
30
+ <p class="text-sm text-muted-foreground"><%= t("ruby_cms.admin.analytics.page", default: "Page") %></p>
31
+ <p class="text-xs text-muted-foreground mt-0.5"><%= time_ago_in_words(event.time) %> ago</p>
32
+ </div>
23
33
  </div>
24
34
  <% end %>
25
35
  <% end %>
26
36
  </div>
27
37
  <% else %>
28
- <div class="px-4 py-10 text-center">
38
+ <div class="flex flex-col items-center justify-center px-4 py-10 gap-2">
39
+ <svg class="w-8 h-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
29
40
  <p class="text-sm font-medium text-foreground"><%= t("ruby_cms.admin.analytics.no_activity", default: "No activity recorded") %></p>
30
- <p class="mt-1 text-sm text-muted-foreground">No page views in selected range.</p>
41
+ <p class="text-xs text-muted-foreground">No page views in selected range</p>
31
42
  </div>
32
43
  <% end %>
33
44
  </div>
@@ -1,21 +1,33 @@
1
- <div class="rounded-lg border border-border/60 bg-white shadow-sm">
2
- <div class="px-4 py-3 border-b border-border/40">
3
- <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.top_referrers", default: "Top Referrers") %></p>
4
- <p class="text-sm text-muted-foreground">Where visitors came from.</p>
1
+ <div class="rounded-xl border border-border/60 bg-white shadow-sm overflow-hidden">
2
+ <div class="flex items-center gap-3 px-4 py-3 border-b border-border/40">
3
+ <div class="rounded-md bg-purple-50 p-1.5 flex-shrink-0">
4
+ <svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
5
+ </div>
6
+ <div>
7
+ <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.top_referrers", default: "Top Referrers") %></p>
8
+ <p class="text-xs text-muted-foreground">Where visitors came from</p>
9
+ </div>
5
10
  </div>
6
11
 
7
- <div class="divide-y divide-border/40">
8
- <% if @top_referrers.present? && @top_referrers.any? %>
12
+ <% if @top_referrers.present? && @top_referrers.any? %>
13
+ <% total = @top_referrers.values.sum.to_f %>
14
+ <div class="divide-y divide-border/30">
9
15
  <% @top_referrers.first(6).each do |referrer, count| %>
10
- <div class="flex items-center justify-between gap-3 px-4 py-2">
11
- <span class="text-xs text-foreground truncate" title="<%= referrer %>"><%= truncate(referrer.to_s.gsub(%r{https?://}, ''), length: 40) %></span>
12
- <span class="text-xs font-semibold tabular-nums text-foreground flex-shrink-0"><%= number_with_delimiter(count) %></span>
16
+ <% pct = total.positive? ? (count / total * 100).round(1) : 0 %>
17
+ <div class="relative flex items-center justify-between gap-3 px-4 py-2.5">
18
+ <div class="absolute inset-y-0 left-0 bg-purple-500/5" style="width: <%= pct %>%"></div>
19
+ <span class="relative z-10 text-sm text-foreground truncate" title="<%= referrer %>"><%= truncate(referrer.to_s.gsub(%r{https?://}, ''), length: 38) %></span>
20
+ <div class="relative z-10 flex items-center gap-2 flex-shrink-0">
21
+ <span class="text-xs text-muted-foreground tabular-nums"><%= pct %>%</span>
22
+ <span class="text-sm font-semibold tabular-nums text-foreground"><%= number_with_delimiter(count) %></span>
23
+ </div>
13
24
  </div>
14
25
  <% end %>
15
- <% else %>
16
- <div class="px-4 py-8 text-center">
17
- <p class="text-sm text-muted-foreground"><%= t("ruby_cms.admin.analytics.no_referrers", default: "No referrers recorded") %></p>
18
- </div>
19
- <% end %>
20
- </div>
26
+ </div>
27
+ <% else %>
28
+ <div class="flex flex-col items-center justify-center px-4 py-10 gap-2">
29
+ <svg class="w-8 h-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
30
+ <p class="text-sm text-muted-foreground"><%= t("ruby_cms.admin.analytics.no_referrers", default: "No referrers recorded") %></p>
31
+ </div>
32
+ <% end %>
21
33
  </div>
@@ -1,28 +1,38 @@
1
- <div class="rounded-lg border border-border/60 bg-white shadow-sm">
2
- <div class="flex items-center justify-between gap-3 px-4 py-3 border-b border-border/40">
1
+ <div class="rounded-xl border border-border/60 bg-white shadow-sm overflow-hidden">
2
+ <div class="flex items-center gap-3 px-4 py-3 border-b border-border/40">
3
+ <div class="rounded-md bg-violet-50 p-1.5 flex-shrink-0">
4
+ <svg class="w-4 h-4 text-violet-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
5
+ </div>
3
6
  <div>
4
7
  <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.top_visitors", default: "Top Visitors") %></p>
5
- <p class="text-sm text-muted-foreground">Most active IPs.</p>
8
+ <p class="text-xs text-muted-foreground">Most active IPs in selected range</p>
6
9
  </div>
7
10
  </div>
8
11
 
9
12
  <% if @top_visitors.present? %>
10
- <div class="divide-y divide-border/40">
13
+ <% max_count = @top_visitors.values.first.to_i %>
14
+ <div class="divide-y divide-border/30">
11
15
  <% @top_visitors.first(8).each_with_index do |(ip, count), idx| %>
16
+ <% pct = max_count.positive? ? (count.to_f / max_count * 100).round(0) : 0 %>
12
17
  <%= link_to visitor_details_ruby_cms_admin_analytics_path(ip_address: ip, start_date: @start_date, end_date: @end_date, period: @period),
13
- class: "group flex items-center justify-between gap-2 px-4 py-2 hover:bg-muted/50 transition-colors" do %>
14
- <div class="flex items-center gap-2 min-w-0">
18
+ class: "group relative flex items-center justify-between gap-3 px-4 py-2.5 hover:bg-muted/40 transition-colors" do %>
19
+ <div class="absolute inset-y-0 left-0 bg-violet-500/5 transition-all" style="width: <%= pct %>%"></div>
20
+ <div class="relative z-10 flex items-center gap-3 min-w-0">
15
21
  <span class="text-xs font-medium text-muted-foreground tabular-nums w-5 text-right flex-shrink-0"><%= idx + 1 %></span>
16
- <p class="text-xs font-medium text-foreground tabular-nums truncate group-hover:text-primary"><%= ip %></p>
22
+ <p class="text-sm font-mono text-foreground truncate group-hover:text-primary transition-colors"><%= ip %></p>
23
+ </div>
24
+ <div class="relative z-10 flex items-center gap-2 flex-shrink-0">
25
+ <span class="text-sm font-semibold tabular-nums text-foreground"><%= number_with_delimiter(count) %></span>
26
+ <svg class="w-3.5 h-3.5 text-muted-foreground/40 group-hover:text-primary/60 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
17
27
  </div>
18
- <span class="text-xs font-semibold tabular-nums text-foreground flex-shrink-0"><%= number_with_delimiter(count) %></span>
19
28
  <% end %>
20
29
  <% end %>
21
30
  </div>
22
31
  <% else %>
23
- <div class="px-4 py-8 text-center">
32
+ <div class="flex flex-col items-center justify-center px-4 py-10 gap-2">
33
+ <svg class="w-8 h-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
24
34
  <p class="text-sm font-medium text-foreground"><%= t("ruby_cms.admin.analytics.no_data", default: "No data yet.") %></p>
25
- <p class="mt-1 text-sm text-muted-foreground">No visitors in selected range.</p>
35
+ <p class="text-xs text-muted-foreground">No visitors in selected range</p>
26
36
  </div>
27
37
  <% end %>
28
38
  </div>
@@ -1,21 +1,33 @@
1
- <div class="rounded-lg border border-border/60 bg-white shadow-sm">
2
- <div class="px-6 py-4 border-b border-border/40">
3
- <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.utm_sources", default: "UTM Sources") %></p>
4
- <p class="text-sm text-muted-foreground">Campaign traffic breakdown.</p>
1
+ <div class="rounded-xl border border-border/60 bg-white shadow-sm overflow-hidden">
2
+ <div class="flex items-center gap-3 px-4 py-3 border-b border-border/40">
3
+ <div class="rounded-md bg-orange-50 p-1.5 flex-shrink-0">
4
+ <svg class="w-4 h-4 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/></svg>
5
+ </div>
6
+ <div>
7
+ <p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.utm_sources", default: "UTM Sources") %></p>
8
+ <p class="text-xs text-muted-foreground">Campaign traffic breakdown</p>
9
+ </div>
5
10
  </div>
6
11
 
7
- <div class="divide-y divide-border/40">
8
- <% if @utm_sources.present? && @utm_sources.any? %>
12
+ <% if @utm_sources.present? && @utm_sources.any? %>
13
+ <% total = @utm_sources.values.sum.to_f %>
14
+ <div class="divide-y divide-border/30">
9
15
  <% @utm_sources.first(6).each do |source, count| %>
10
- <div class="flex items-center justify-between gap-3 px-6 py-3">
11
- <span class="text-sm text-foreground truncate"><%= source %></span>
12
- <span class="text-sm font-semibold tabular-nums text-foreground flex-shrink-0"><%= number_with_delimiter(count) %></span>
16
+ <% pct = total.positive? ? (count / total * 100).round(1) : 0 %>
17
+ <div class="relative flex items-center justify-between gap-3 px-4 py-2.5">
18
+ <div class="absolute inset-y-0 left-0 bg-orange-500/5" style="width: <%= pct %>%"></div>
19
+ <span class="relative z-10 text-sm text-foreground truncate"><%= source %></span>
20
+ <div class="relative z-10 flex items-center gap-2 flex-shrink-0">
21
+ <span class="text-xs text-muted-foreground tabular-nums"><%= pct %>%</span>
22
+ <span class="text-sm font-semibold tabular-nums text-foreground"><%= number_with_delimiter(count) %></span>
23
+ </div>
13
24
  </div>
14
25
  <% end %>
15
- <% else %>
16
- <div class="px-6 py-10 text-center">
17
- <p class="text-sm text-muted-foreground">No UTM campaign data</p>
18
- </div>
19
- <% end %>
20
- </div>
26
+ </div>
27
+ <% else %>
28
+ <div class="flex flex-col items-center justify-center px-4 py-10 gap-2">
29
+ <svg class="w-8 h-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/></svg>
30
+ <p class="text-sm text-muted-foreground">No UTM campaign data</p>
31
+ </div>
32
+ <% end %>
21
33
  </div>
@@ -29,8 +29,15 @@
29
29
  end
30
30
  %>
31
31
 
32
+ <div class="mb-4">
33
+ <h1 class="text-2xl font-semibold text-foreground">Content blocks</h1>
34
+ <p class="mt-1 text-sm text-muted-foreground">
35
+ <%= number_with_delimiter(@pagination&.dig(:total_count) || @content_blocks&.size || 0) %> total items
36
+ </p>
37
+ </div>
38
+
32
39
  <%= render partial: "ruby_cms/admin/shared/bulk_action_table_index", locals: {
33
- title: "Content blocks",
40
+ title: nil,
34
41
  collection: @content_blocks || [],
35
42
  headers: ["Key", "Locale", "Title", "Type", "Published", { text: "Actions", class: "text-right" }],
36
43
  action_icons: [