ruby_cms 0.1.2 → 0.1.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/app/components/ruby_cms/admin/admin_page_header.rb +2 -4
- data/app/components/ruby_cms/admin/admin_resource_card.rb +2 -2
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +37 -24
- data/app/helpers/ruby_cms/content_blocks_helper.rb +7 -3
- data/app/helpers/ruby_cms/settings_helper.rb +19 -17
- data/app/models/ruby_cms/permission.rb +1 -1
- data/app/services/ruby_cms/analytics/report.rb +37 -3
- data/app/views/ruby_cms/admin/analytics/index.html.erb +103 -108
- data/app/views/ruby_cms/admin/analytics/page_details.html.erb +28 -32
- data/app/views/ruby_cms/admin/analytics/partials/_back_button.html.erb +3 -2
- data/app/views/ruby_cms/admin/analytics/partials/_browser_device.html.erb +34 -20
- data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +27 -27
- data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +19 -19
- data/app/views/ruby_cms/admin/analytics/partials/_landing_pages.html.erb +21 -0
- data/app/views/ruby_cms/admin/analytics/partials/_os_stats.html.erb +26 -0
- data/app/views/ruby_cms/admin/analytics/partials/_popular_pages.html.erb +34 -0
- data/app/views/ruby_cms/admin/analytics/partials/_recent_activity.html.erb +16 -14
- data/app/views/ruby_cms/admin/analytics/partials/_security_alert.html.erb +3 -3
- data/app/views/ruby_cms/admin/analytics/partials/_top_referrers.html.erb +14 -14
- data/app/views/ruby_cms/admin/analytics/partials/_top_visitors.html.erb +28 -0
- data/app/views/ruby_cms/admin/analytics/partials/_utm_sources.html.erb +21 -0
- data/app/views/ruby_cms/admin/analytics/visitor_details.html.erb +44 -48
- data/lib/generators/ruby_cms/install_generator.rb +2 -1
- data/lib/ruby_cms/cli.rb +1 -1
- data/lib/ruby_cms/version.rb +1 -1
- data/lib/tasks/admin.rake +120 -0
- data/log/test.log +7284 -0
- metadata +7 -2
- data/lib/tasks/ruby_cms.rake +0 -27
|
@@ -4,76 +4,72 @@
|
|
|
4
4
|
show_security = high_volume || suspicious_ua
|
|
5
5
|
%>
|
|
6
6
|
<%= admin_page(
|
|
7
|
-
title:
|
|
8
|
-
subtitle: "#{@start_date} → #{@end_date}",
|
|
7
|
+
title: @page_name.to_s.humanize,
|
|
8
|
+
subtitle: "Page analytics · #{@start_date} → #{@end_date}",
|
|
9
9
|
content_card: false
|
|
10
10
|
) do %>
|
|
11
11
|
<div class="space-y-6">
|
|
12
12
|
<div class="flex items-start justify-between gap-4">
|
|
13
|
-
<div class="
|
|
14
|
-
<p class="text-sm font-medium text-gray-500">Page</p>
|
|
15
|
-
<p class="mt-1 text-lg font-semibold tracking-tight text-gray-900 truncate"><%= @page_name %></p>
|
|
16
|
-
</div>
|
|
17
|
-
<div class="flex items-center gap-2 flex-shrink-0">
|
|
13
|
+
<div class="flex items-center gap-2">
|
|
18
14
|
<%= render "ruby_cms/admin/analytics/partials/security_alert" if show_security %>
|
|
19
|
-
<%= render "ruby_cms/admin/analytics/partials/back_button", path: ruby_cms_admin_analytics_path(start_date: @start_date, end_date: @end_date, period: @period) %>
|
|
20
15
|
</div>
|
|
16
|
+
<%= render "ruby_cms/admin/analytics/partials/back_button", path: ruby_cms_admin_analytics_path(start_date: @start_date, end_date: @end_date, period: @period) %>
|
|
21
17
|
</div>
|
|
22
18
|
|
|
23
19
|
<div class="grid grid-cols-3 gap-4">
|
|
24
|
-
<div class="rounded-lg border border-
|
|
25
|
-
<p class="text-sm font-medium text-
|
|
26
|
-
<p class="mt-2 text-
|
|
20
|
+
<div class="rounded-lg border border-border/60 bg-white p-6 shadow-sm">
|
|
21
|
+
<p class="text-sm font-medium text-muted-foreground">Total views</p>
|
|
22
|
+
<p class="mt-2 text-2xl font-semibold tracking-tight text-foreground tabular-nums"><%= number_with_delimiter(@page_stats[:total_views]) %></p>
|
|
27
23
|
</div>
|
|
28
|
-
<div class="rounded-lg border border-
|
|
29
|
-
<p class="text-sm font-medium text-
|
|
30
|
-
<p class="mt-2 text-
|
|
24
|
+
<div class="rounded-lg border border-border/60 bg-white p-6 shadow-sm">
|
|
25
|
+
<p class="text-sm font-medium text-muted-foreground">Unique visitors</p>
|
|
26
|
+
<p class="mt-2 text-2xl font-semibold tracking-tight text-foreground tabular-nums"><%= number_with_delimiter(@page_stats[:unique_visitors]) %></p>
|
|
31
27
|
</div>
|
|
32
|
-
<div class="rounded-lg border border-
|
|
33
|
-
<p class="text-sm font-medium text-
|
|
34
|
-
<p class="mt-2 text-
|
|
28
|
+
<div class="rounded-lg border border-border/60 bg-white p-6 shadow-sm">
|
|
29
|
+
<p class="text-sm font-medium text-muted-foreground">Avg / day</p>
|
|
30
|
+
<p class="mt-2 text-2xl font-semibold tracking-tight text-foreground tabular-nums"><%= number_with_precision(@page_stats[:avg_views_per_day], precision: 1) %></p>
|
|
35
31
|
</div>
|
|
36
32
|
</div>
|
|
37
33
|
|
|
38
|
-
<div class="rounded-lg border border-
|
|
39
|
-
<div class="px-6 py-4 border-b border-
|
|
40
|
-
<p class="text-sm font-semibold text-
|
|
41
|
-
<p class="text-sm text-
|
|
34
|
+
<div class="rounded-lg border border-border/60 bg-white shadow-sm overflow-hidden">
|
|
35
|
+
<div class="px-6 py-4 border-b border-border/40">
|
|
36
|
+
<p class="text-sm font-semibold text-foreground">Page views</p>
|
|
37
|
+
<p class="text-sm text-muted-foreground">Recent views for this page.</p>
|
|
42
38
|
</div>
|
|
43
39
|
|
|
44
40
|
<div class="overflow-x-auto">
|
|
45
41
|
<table class="min-w-full text-sm">
|
|
46
|
-
<thead class="bg-
|
|
47
|
-
<tr class="text-left text-xs font-
|
|
42
|
+
<thead class="bg-muted/30">
|
|
43
|
+
<tr class="text-left text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
|
48
44
|
<th class="px-6 py-3">IP Address</th>
|
|
49
45
|
<th class="px-6 py-3">Browser</th>
|
|
50
46
|
<th class="px-6 py-3">Referrer</th>
|
|
51
47
|
<th class="px-6 py-3">Time</th>
|
|
52
48
|
</tr>
|
|
53
49
|
</thead>
|
|
54
|
-
<tbody class="divide-y divide-
|
|
50
|
+
<tbody class="divide-y divide-border/40">
|
|
55
51
|
<% if @page_views.any? %>
|
|
56
52
|
<% @page_views.each do |event| %>
|
|
57
53
|
<% visit = event.visit %>
|
|
58
54
|
<% ip = visit&.ip %>
|
|
59
|
-
<tr class="hover:bg-
|
|
60
|
-
<td class="px-6 py-3 font-medium text-
|
|
55
|
+
<tr class="hover:bg-muted/30 transition-colors">
|
|
56
|
+
<td class="px-6 py-3 font-medium text-foreground tabular-nums">
|
|
61
57
|
<% if ip.present? && ip != "Unknown" %>
|
|
62
58
|
<%= link_to ip,
|
|
63
59
|
visitor_details_ruby_cms_admin_analytics_path(ip_address: ip, start_date: @start_date, end_date: @end_date, period: @period),
|
|
64
|
-
class: "text-
|
|
60
|
+
class: "text-primary hover:underline" %>
|
|
65
61
|
<% else %>
|
|
66
|
-
<span class="text-
|
|
62
|
+
<span class="text-muted-foreground"><%= ip || "Unknown" %></span>
|
|
67
63
|
<% end %>
|
|
68
64
|
</td>
|
|
69
|
-
<td class="px-6 py-3 text-
|
|
70
|
-
<td class="px-6 py-3 text-
|
|
71
|
-
<td class="px-6 py-3 text-
|
|
65
|
+
<td class="px-6 py-3 text-foreground"><%= visit&.browser || "Unknown" %></td>
|
|
66
|
+
<td class="px-6 py-3 text-foreground truncate max-w-[26rem]"><%= visit&.referrer.presence || "Direct" %></td>
|
|
67
|
+
<td class="px-6 py-3 text-muted-foreground"><%= time_ago_in_words(event.time) %> ago</td>
|
|
72
68
|
</tr>
|
|
73
69
|
<% end %>
|
|
74
70
|
<% else %>
|
|
75
71
|
<tr>
|
|
76
|
-
<td colspan="4" class="px-6 py-10 text-center text-sm text-
|
|
72
|
+
<td colspan="4" class="px-6 py-10 text-center text-sm text-muted-foreground">No page views recorded for this page</td>
|
|
77
73
|
</tr>
|
|
78
74
|
<% end %>
|
|
79
75
|
</tbody>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
<%= link_to path, class: "inline-flex items-center justify-center rounded-md border border-
|
|
2
|
-
|
|
1
|
+
<%= link_to path, class: "inline-flex items-center justify-center rounded-md border border-border bg-white px-3 py-2 text-sm font-medium text-foreground shadow-sm hover:bg-muted transition-colors" do %>
|
|
2
|
+
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
|
3
|
+
<%= t("ruby_cms.admin.analytics.back", default: "Back") %>
|
|
3
4
|
<% end %>
|
|
@@ -1,40 +1,54 @@
|
|
|
1
|
-
<div class="rounded-lg border border-
|
|
2
|
-
<div class="px-
|
|
3
|
-
<p class="text-sm font-semibold text-
|
|
4
|
-
<p class="text-sm text-
|
|
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.browsers_devices", default: "Browsers & Devices") %></p>
|
|
4
|
+
<p class="text-sm text-muted-foreground">Environment breakdown.</p>
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
|
-
<div class="px-
|
|
7
|
+
<div class="px-4 py-3 space-y-3">
|
|
8
8
|
<% if @browser_stats.present? && @browser_stats.any? %>
|
|
9
9
|
<div>
|
|
10
|
-
<p class="text-xs font-
|
|
11
|
-
|
|
10
|
+
<p class="text-xs font-medium uppercase tracking-wider text-muted-foreground mb-2">Browsers</p>
|
|
11
|
+
<% total = @browser_stats.values.sum.to_f %>
|
|
12
|
+
<div class="space-y-2.5">
|
|
12
13
|
<% @browser_stats.sort_by { |_, v| -v }.first(4).each do |browser, count| %>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<
|
|
16
|
-
|
|
14
|
+
<% pct = total.positive? ? (count / total * 100).round(1) : 0 %>
|
|
15
|
+
<div>
|
|
16
|
+
<div class="flex items-center justify-between gap-3 mb-1">
|
|
17
|
+
<span class="text-sm text-foreground"><%= browser %></span>
|
|
18
|
+
<div class="flex items-center gap-2 flex-shrink-0">
|
|
19
|
+
<span class="text-xs text-muted-foreground tabular-nums"><%= pct %>%</span>
|
|
20
|
+
<span class="text-sm font-semibold tabular-nums text-foreground"><%= number_with_delimiter(count) %></span>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="w-full h-1 bg-muted rounded-full overflow-hidden">
|
|
24
|
+
<div class="h-full bg-primary/60 rounded-full" style="width: <%= pct %>%"></div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
17
27
|
<% end %>
|
|
18
|
-
</
|
|
28
|
+
</div>
|
|
19
29
|
</div>
|
|
20
30
|
<% end %>
|
|
21
31
|
|
|
22
32
|
<% if @device_stats.present? && @device_stats.any? %>
|
|
23
33
|
<div>
|
|
24
|
-
<p class="text-xs font-
|
|
25
|
-
|
|
34
|
+
<p class="text-xs font-medium uppercase tracking-wider text-muted-foreground mb-2">Devices</p>
|
|
35
|
+
<% total = @device_stats.values.sum.to_f %>
|
|
36
|
+
<div class="flex gap-3">
|
|
26
37
|
<% @device_stats.sort_by { |_, v| -v }.first(4).each do |device, count| %>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
<
|
|
30
|
-
|
|
38
|
+
<% pct = total.positive? ? (count / total * 100).round(0) : 0 %>
|
|
39
|
+
<div class="flex-1 rounded-lg border border-border/40 bg-muted/30 p-2.5 text-center">
|
|
40
|
+
<p class="text-lg font-semibold text-foreground tabular-nums"><%= pct %>%</p>
|
|
41
|
+
<p class="text-xs text-muted-foreground mt-0.5"><%= device %></p>
|
|
42
|
+
</div>
|
|
31
43
|
<% end %>
|
|
32
|
-
</
|
|
44
|
+
</div>
|
|
33
45
|
</div>
|
|
34
46
|
<% end %>
|
|
35
47
|
|
|
36
48
|
<% if (@browser_stats.blank? || @browser_stats.empty?) && (@device_stats.blank? || @device_stats.empty?) %>
|
|
37
|
-
<
|
|
49
|
+
<div class="py-6 text-center">
|
|
50
|
+
<p class="text-sm text-muted-foreground"><%= t("ruby_cms.admin.analytics.no_data", default: "No data yet.") %></p>
|
|
51
|
+
</div>
|
|
38
52
|
<% end %>
|
|
39
53
|
</div>
|
|
40
54
|
</div>
|
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
<div class="rounded-lg border border-
|
|
2
|
-
<div class="flex items-
|
|
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
3
|
<div>
|
|
4
|
-
<p class="text-sm font-semibold text-
|
|
5
|
-
<p class="text-sm text-
|
|
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>
|
|
6
6
|
</div>
|
|
7
|
-
<div class="flex items-center gap-
|
|
8
|
-
<span class="inline-flex items-center gap-
|
|
9
|
-
<span class="inline-flex items-center gap-
|
|
7
|
+
<div class="flex items-center gap-4 text-sm text-muted-foreground">
|
|
8
|
+
<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
|
+
<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
10
|
</div>
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
|
-
<div class="px-
|
|
13
|
+
<div class="px-4 py-3">
|
|
14
14
|
<% if @daily_activity.present? && @daily_activity.any? %>
|
|
15
15
|
<%
|
|
16
|
-
|
|
17
|
-
max_views =
|
|
16
|
+
daily_arr = @daily_activity.is_a?(Hash) ? @daily_activity.to_a : @daily_activity
|
|
17
|
+
max_views = daily_arr.map { |_k, v| v.to_i }.max || 0
|
|
18
18
|
max_visitors = (@daily_visitors || {}).values.map(&:to_i).max || 0
|
|
19
19
|
max_value = [max_views, max_visitors].max
|
|
20
20
|
max_value = 1 if max_value.zero?
|
|
21
|
-
|
|
21
|
+
chart_h = 130
|
|
22
22
|
%>
|
|
23
23
|
|
|
24
|
-
<div class="grid grid-cols-[
|
|
25
|
-
<div class="flex flex-col justify-between h-[<%=
|
|
24
|
+
<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">
|
|
26
26
|
<% [100, 75, 50, 25, 0].each do |pct| %>
|
|
27
|
-
<div class="
|
|
27
|
+
<div class="text-right"><%= number_with_delimiter((max_value * pct / 100.0).round) %></div>
|
|
28
28
|
<% end %>
|
|
29
29
|
</div>
|
|
30
30
|
|
|
31
31
|
<div class="relative">
|
|
32
32
|
<div class="absolute inset-0 flex flex-col justify-between pointer-events-none">
|
|
33
|
-
<% 5.times do %>
|
|
34
|
-
<div class="h-px bg-gray-100"></div>
|
|
35
|
-
<% end %>
|
|
33
|
+
<% 5.times do %><div class="h-px bg-border/40"></div><% end %>
|
|
36
34
|
</div>
|
|
37
35
|
|
|
38
|
-
<div class="h-[<%=
|
|
39
|
-
<%
|
|
36
|
+
<div class="h-[<%= chart_h %>px] flex items-end gap-1">
|
|
37
|
+
<% daily_arr.each do |date, views_count| %>
|
|
40
38
|
<% visitors_count = (@daily_visitors || {})[date] || 0 %>
|
|
41
|
-
<% views_h = [((views_count.to_f / max_value) *
|
|
42
|
-
<% visitors_h = [((visitors_count.to_f / max_value) *
|
|
43
|
-
<div class="flex flex-col items-center gap-
|
|
44
|
-
<div class="w-full flex items-end justify-center gap-
|
|
45
|
-
<div class="w-
|
|
46
|
-
<div class="w-
|
|
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 %>
|
|
41
|
+
<div class="flex flex-col items-center gap-1.5 flex-1 min-w-0 group">
|
|
42
|
+
<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>
|
|
47
45
|
</div>
|
|
48
|
-
<div class="text-[
|
|
46
|
+
<div class="text-[10px] text-muted-foreground truncate w-full text-center"><%= format_chart_date_short(date) %></div>
|
|
49
47
|
</div>
|
|
50
48
|
<% end %>
|
|
51
49
|
</div>
|
|
52
50
|
</div>
|
|
53
51
|
</div>
|
|
54
52
|
<% else %>
|
|
55
|
-
<
|
|
53
|
+
<div class="flex items-center justify-center h-40">
|
|
54
|
+
<p class="text-sm text-muted-foreground"><%= t("ruby_cms.admin.analytics.no_daily_data", default: "No daily activity data") %></p>
|
|
55
|
+
</div>
|
|
56
56
|
<% end %>
|
|
57
57
|
</div>
|
|
58
58
|
</div>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
<div class="rounded-lg border border-
|
|
2
|
-
<div class="px-
|
|
3
|
-
<p class="text-sm font-semibold text-
|
|
4
|
-
<p class="text-sm text-
|
|
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>
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
|
-
<div class="px-
|
|
7
|
+
<div class="px-4 py-3">
|
|
8
8
|
<% if @hourly_activity.present? && @hourly_activity.any? %>
|
|
9
9
|
<%
|
|
10
10
|
hourly_arr = @hourly_activity.is_a?(Hash) ? @hourly_activity.to_a : @hourly_activity
|
|
@@ -14,38 +14,38 @@
|
|
|
14
14
|
[hours, total]
|
|
15
15
|
end
|
|
16
16
|
max_value = grouped.map { |_, c| c }.max || 1
|
|
17
|
-
|
|
17
|
+
chart_h = 130
|
|
18
18
|
%>
|
|
19
19
|
|
|
20
|
-
<div class="grid grid-cols-[
|
|
21
|
-
<div class="flex flex-col justify-between h-[<%=
|
|
20
|
+
<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">
|
|
22
22
|
<% [100, 75, 50, 25, 0].each do |pct| %>
|
|
23
|
-
<div class="
|
|
23
|
+
<div class="text-right"><%= number_with_delimiter((max_value * pct / 100.0).round) %></div>
|
|
24
24
|
<% end %>
|
|
25
25
|
</div>
|
|
26
26
|
|
|
27
27
|
<div class="relative">
|
|
28
28
|
<div class="absolute inset-0 flex flex-col justify-between pointer-events-none">
|
|
29
|
-
<% 5.times do %>
|
|
30
|
-
<div class="h-px bg-gray-100"></div>
|
|
31
|
-
<% end %>
|
|
29
|
+
<% 5.times do %><div class="h-px bg-border/40"></div><% end %>
|
|
32
30
|
</div>
|
|
33
31
|
|
|
34
|
-
<div class="h-[<%=
|
|
32
|
+
<div class="h-[<%= chart_h %>px] flex items-end gap-1.5">
|
|
35
33
|
<% grouped.each do |hours, count| %>
|
|
36
|
-
<% h = [((count.to_f / max_value) *
|
|
37
|
-
<div class="flex flex-col items-center gap-
|
|
38
|
-
<div class="w-full flex items-end justify-center" title="<%= hours.size > 1 ? "#{hours.first}:00
|
|
39
|
-
<div class="w-
|
|
34
|
+
<% h = [((count.to_f / max_value) * chart_h).round, 2].max %>
|
|
35
|
+
<div class="flex flex-col items-center gap-1.5 flex-1 min-w-0 group">
|
|
36
|
+
<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>
|
|
40
38
|
</div>
|
|
41
|
-
<div class="text-[
|
|
39
|
+
<div class="text-[10px] text-muted-foreground truncate w-full text-center"><%= hours.size > 1 ? "#{hours.first}–#{hours.last}h" : "#{hours.first}h" %></div>
|
|
42
40
|
</div>
|
|
43
41
|
<% end %>
|
|
44
42
|
</div>
|
|
45
43
|
</div>
|
|
46
44
|
</div>
|
|
47
45
|
<% else %>
|
|
48
|
-
<
|
|
46
|
+
<div class="flex items-center justify-center h-40">
|
|
47
|
+
<p class="text-sm text-muted-foreground"><%= t("ruby_cms.admin.analytics.no_hourly_data", default: "No hourly activity data") %></p>
|
|
48
|
+
</div>
|
|
49
49
|
<% end %>
|
|
50
50
|
</div>
|
|
51
51
|
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
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>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="divide-y divide-border/40">
|
|
8
|
+
<% if @landing_pages.present? && @landing_pages.any? %>
|
|
9
|
+
<% @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>
|
|
13
|
+
</div>
|
|
14
|
+
<% 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>
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
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>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="divide-y divide-border/40">
|
|
8
|
+
<% if @os_stats.present? && @os_stats.any? %>
|
|
9
|
+
<% total = @os_stats.values.sum.to_f %>
|
|
10
|
+
<% @os_stats.sort_by { |_, v| -v }.first(6).each do |os, count| %>
|
|
11
|
+
<% 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>
|
|
19
|
+
<% 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>
|
|
26
|
+
</div>
|
|
@@ -0,0 +1,34 @@
|
|
|
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">
|
|
3
|
+
<div>
|
|
4
|
+
<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>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<% if @popular_pages.present? %>
|
|
10
|
+
<% total = @popular_pages.values.sum.to_f %>
|
|
11
|
+
<div class="divide-y divide-border/40">
|
|
12
|
+
<% @popular_pages.first(8).each_with_index do |(page_name, count), idx| %>
|
|
13
|
+
<% pct = total.positive? ? (count / total * 100).round(1) : 0 %>
|
|
14
|
+
<%= 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
|
+
<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>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="relative z-10 flex items-center gap-2 flex-shrink-0">
|
|
22
|
+
<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>
|
|
24
|
+
</div>
|
|
25
|
+
<% end %>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
28
|
+
<% else %>
|
|
29
|
+
<div class="px-4 py-8 text-center">
|
|
30
|
+
<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>
|
|
32
|
+
</div>
|
|
33
|
+
<% end %>
|
|
34
|
+
</div>
|
|
@@ -1,31 +1,33 @@
|
|
|
1
|
-
<div class="rounded-lg border border-
|
|
2
|
-
<div class="px-
|
|
3
|
-
<p class="text-sm font-semibold text-
|
|
4
|
-
<p class="text-sm text-
|
|
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>
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
7
|
<% if @recent_page_views.present? && @recent_page_views.any? %>
|
|
8
|
-
<div class="divide-y divide-
|
|
8
|
+
<div class="divide-y divide-border/40">
|
|
9
9
|
<% @recent_page_views.first(6).each do |event| %>
|
|
10
10
|
<% page_name = event.respond_to?(:page_name) ? event.page_name : nil %>
|
|
11
11
|
<% if page_name.present? %>
|
|
12
12
|
<%= 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: "
|
|
14
|
-
<
|
|
15
|
-
|
|
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>
|
|
17
|
+
</div>
|
|
16
18
|
<% end %>
|
|
17
19
|
<% else %>
|
|
18
|
-
<div class="px-
|
|
19
|
-
<p class="text-
|
|
20
|
-
<p class="
|
|
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>
|
|
21
23
|
</div>
|
|
22
24
|
<% end %>
|
|
23
25
|
<% end %>
|
|
24
26
|
</div>
|
|
25
27
|
<% else %>
|
|
26
|
-
<div class="px-
|
|
27
|
-
<p class="text-sm font-medium text-
|
|
28
|
-
<p class="mt-1 text-sm text-
|
|
28
|
+
<div class="px-4 py-10 text-center">
|
|
29
|
+
<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>
|
|
29
31
|
</div>
|
|
30
32
|
<% end %>
|
|
31
33
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<div class="inline-flex items-center gap-
|
|
2
|
-
<
|
|
3
|
-
|
|
1
|
+
<div class="inline-flex items-center gap-1.5 rounded-md border border-amber-200 bg-amber-50 px-3 py-1.5 text-sm font-medium text-amber-900">
|
|
2
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
|
|
3
|
+
<%= t("ruby_cms.admin.analytics.security_alert", default: "Security alert") %>
|
|
4
4
|
</div>
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
<div class="rounded-lg border border-
|
|
2
|
-
<div class="px-
|
|
3
|
-
<p class="text-sm font-semibold text-
|
|
4
|
-
<p class="text-sm text-
|
|
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>
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
|
-
<div class="
|
|
7
|
+
<div class="divide-y divide-border/40">
|
|
8
8
|
<% if @top_referrers.present? && @top_referrers.any? %>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<% end %>
|
|
16
|
-
</ul>
|
|
9
|
+
<% @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>
|
|
13
|
+
</div>
|
|
14
|
+
<% end %>
|
|
17
15
|
<% else %>
|
|
18
|
-
<
|
|
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
19
|
<% end %>
|
|
20
20
|
</div>
|
|
21
21
|
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
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">
|
|
3
|
+
<div>
|
|
4
|
+
<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>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<% if @top_visitors.present? %>
|
|
10
|
+
<div class="divide-y divide-border/40">
|
|
11
|
+
<% @top_visitors.first(8).each_with_index do |(ip, count), idx| %>
|
|
12
|
+
<%= 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">
|
|
15
|
+
<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>
|
|
17
|
+
</div>
|
|
18
|
+
<span class="text-xs font-semibold tabular-nums text-foreground flex-shrink-0"><%= number_with_delimiter(count) %></span>
|
|
19
|
+
<% end %>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
<% else %>
|
|
23
|
+
<div class="px-4 py-8 text-center">
|
|
24
|
+
<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>
|
|
26
|
+
</div>
|
|
27
|
+
<% end %>
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
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>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="divide-y divide-border/40">
|
|
8
|
+
<% if @utm_sources.present? && @utm_sources.any? %>
|
|
9
|
+
<% @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>
|
|
13
|
+
</div>
|
|
14
|
+
<% 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>
|
|
21
|
+
</div>
|