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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b498f6774c5c9d39e255eb655c136925ac8af538a48edb49c9db3421c17388bb
|
|
4
|
+
data.tar.gz: e9d077f831552bdf815afa1af9468b5dd895fed1f809cb7f2a748c44492c6c66
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 057ace1576f400a416183a52bb14651c71bab4384ceeddcbf951b1ab17d781c8548fe41af9b45ccdadef33515f42399241568ba480d69cbb056adcc46e1e5a5a
|
|
7
|
+
data.tar.gz: 7a02feff4b17fc21391c50c0c745e90a9e4d653cf53d1ed0c7ab6f74ddb599787585fd9353c623fa7a9c4457e034ab286d8e9e90b0e1fe7017da67cce11c82ce
|
data/CHANGELOG.md
CHANGED
|
@@ -47,8 +47,8 @@ module RubyCms
|
|
|
47
47
|
CANCEL_CLASS = "inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg border border-border bg-background text-foreground hover:bg-muted transition-colors"
|
|
48
48
|
SUBMIT_CLASS = "inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm transition-colors"
|
|
49
49
|
|
|
50
|
-
def view_template(&
|
|
51
|
-
div(class: CARD_CLASS, &
|
|
50
|
+
def view_template(&)
|
|
51
|
+
div(class: CARD_CLASS, &)
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
end
|
|
@@ -42,36 +42,49 @@ module RubyCms
|
|
|
42
42
|
|
|
43
43
|
def render_selection_info
|
|
44
44
|
div(class: "flex items-center gap-3") do
|
|
45
|
-
|
|
46
|
-
div(class: "size-6 rounded-full bg-primary/10 flex items-center justify-center") do
|
|
47
|
-
span(
|
|
48
|
-
class: "text-xs font-bold text-primary tabular-nums",
|
|
49
|
-
data: {
|
|
50
|
-
"#{@controller_name}-target": "selectedCount"
|
|
51
|
-
}
|
|
52
|
-
) { "0" }
|
|
53
|
-
end
|
|
54
|
-
span(class: "text-sm font-medium text-foreground") { "selected" }
|
|
55
|
-
end
|
|
45
|
+
render_selection_left
|
|
56
46
|
div(class: "h-4 w-px bg-border/60")
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
) { "
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
47
|
+
render_selection_buttons
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def render_selection_left
|
|
52
|
+
div(class: "flex items-center gap-2") do
|
|
53
|
+
render_selected_count_badge
|
|
54
|
+
span(class: "text-sm font-medium text-foreground") { "selected" }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def render_selected_count_badge
|
|
59
|
+
div(class: "size-6 rounded-full bg-primary/10 flex items-center justify-center") do
|
|
60
|
+
span(
|
|
61
|
+
class: "text-xs font-bold text-primary tabular-nums",
|
|
68
62
|
data: {
|
|
69
|
-
|
|
63
|
+
"#{@controller_name}-target": "selectedCount"
|
|
70
64
|
}
|
|
71
|
-
) { "
|
|
65
|
+
) { "0" }
|
|
72
66
|
end
|
|
73
67
|
end
|
|
74
68
|
|
|
69
|
+
def render_selection_buttons
|
|
70
|
+
button(
|
|
71
|
+
type: "button",
|
|
72
|
+
class: "text-xs font-medium text-muted-foreground hover:text-foreground transition-colors",
|
|
73
|
+
data: {
|
|
74
|
+
"#{@controller_name}-target": "selectAllButton",
|
|
75
|
+
action: "click->#{@controller_name}#selectAll"
|
|
76
|
+
}
|
|
77
|
+
) { "Select all" }
|
|
78
|
+
|
|
79
|
+
button(
|
|
80
|
+
type: "button",
|
|
81
|
+
class: "text-xs font-medium text-muted-foreground hover:text-foreground transition-colors",
|
|
82
|
+
data: {
|
|
83
|
+
action: "click->#{@controller_name}#clearSelection"
|
|
84
|
+
}
|
|
85
|
+
) { "Clear" }
|
|
86
|
+
end
|
|
87
|
+
|
|
75
88
|
def render_action_buttons
|
|
76
89
|
div(class: "flex items-center gap-2") do
|
|
77
90
|
@bulk_action_buttons.each do |button_config|
|
|
@@ -114,14 +114,14 @@ module RubyCms
|
|
|
114
114
|
def build_css_class(options)
|
|
115
115
|
user_class = options.delete(:class)
|
|
116
116
|
return user_class.to_s if user_class.present? && !visual_editor_preview_edit_mode?
|
|
117
|
-
return user_class.to_s
|
|
117
|
+
return user_class.to_s unless visual_editor_preview_edit_mode?
|
|
118
118
|
|
|
119
119
|
["ruby_cms-content-block", "content-block", user_class].compact.join(" ")
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
def build_data_attributes(key, options)
|
|
123
123
|
user_data = options.delete(:data).to_h
|
|
124
|
-
return user_data
|
|
124
|
+
return user_data unless visual_editor_preview_edit_mode?
|
|
125
125
|
|
|
126
126
|
{ content_key: key, block_id: key.to_s }.merge(user_data)
|
|
127
127
|
end
|
|
@@ -133,7 +133,11 @@ module RubyCms
|
|
|
133
133
|
return false unless controller.respond_to?(:action_name) && controller.action_name.to_s == "page_preview"
|
|
134
134
|
|
|
135
135
|
begin
|
|
136
|
-
|
|
136
|
+
if controller.instance_variable_defined?(:@edit_mode)
|
|
137
|
+
controller.instance_variable_get(:@edit_mode) ? true : false
|
|
138
|
+
else
|
|
139
|
+
true
|
|
140
|
+
end
|
|
137
141
|
rescue StandardError
|
|
138
142
|
true
|
|
139
143
|
end
|
|
@@ -40,7 +40,7 @@ module RubyCms
|
|
|
40
40
|
return nil unless key_str.start_with?("nav_show_")
|
|
41
41
|
|
|
42
42
|
nav_key = key_str.delete_prefix("nav_show_")
|
|
43
|
-
item = RubyCms.nav_registry.find {
|
|
43
|
+
item = RubyCms.nav_registry.find {|e| e[:key].to_s == nav_key }
|
|
44
44
|
item&.dig(:icon)
|
|
45
45
|
end
|
|
46
46
|
|
|
@@ -85,31 +85,33 @@ module RubyCms
|
|
|
85
85
|
|
|
86
86
|
def render_boolean_setting_field(entry:, value:, tab:)
|
|
87
87
|
checked = ActiveModel::Type::Boolean.new.cast(value)
|
|
88
|
+
input_id = setting_input_id(entry)
|
|
89
|
+
|
|
88
90
|
hidden = hidden_field_tag("preferences[#{entry.key}]", "false")
|
|
89
91
|
checkbox = check_box_tag(
|
|
90
92
|
"preferences[#{entry.key}]",
|
|
91
93
|
"true",
|
|
92
94
|
checked,
|
|
93
|
-
id:
|
|
95
|
+
id: input_id,
|
|
94
96
|
class: "peer sr-only",
|
|
95
97
|
data: autosave_data(entry.key, tab)
|
|
96
98
|
)
|
|
97
|
-
label = content_tag(
|
|
98
|
-
:label,
|
|
99
|
-
"",
|
|
100
|
-
for: setting_input_id(entry),
|
|
101
|
-
class: "relative inline-flex h-7 w-12 cursor-pointer items-center rounded-full " \
|
|
102
|
-
"border border-border bg-muted transition-colors peer-checked:bg-primary peer-checked:border-primary"
|
|
103
|
-
) do
|
|
104
|
-
content_tag(
|
|
105
|
-
:span,
|
|
106
|
-
"",
|
|
107
|
-
class: "inline-block h-5 w-5 transform rounded-full bg-background shadow-sm ring-1 ring-border transition " \
|
|
108
|
-
"translate-x-1 peer-checked:translate-x-6"
|
|
109
|
-
)
|
|
110
|
-
end
|
|
111
99
|
|
|
112
|
-
content_tag(:
|
|
100
|
+
track = content_tag(:label, "", for: input_id,
|
|
101
|
+
class: "relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full " \
|
|
102
|
+
"border-2 border-transparent bg-input transition-colors " \
|
|
103
|
+
"peer-checked:bg-primary " \
|
|
104
|
+
"peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-ring " \
|
|
105
|
+
"peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-background")
|
|
106
|
+
|
|
107
|
+
thumb = content_tag(:span, "",
|
|
108
|
+
class: "pointer-events-none absolute left-[3px] top-1/2 -translate-y-1/2 h-5 w-5 rounded-full " \
|
|
109
|
+
"bg-background shadow-lg ring-0 transition-transform " \
|
|
110
|
+
"translate-x-0 peer-checked:translate-x-5")
|
|
111
|
+
|
|
112
|
+
content_tag(:div, class: "relative inline-flex items-center shrink-0") do
|
|
113
|
+
safe_join([hidden, checkbox, track, thumb])
|
|
114
|
+
end
|
|
113
115
|
end
|
|
114
116
|
|
|
115
117
|
def render_json_setting_field(entry:, value:, tab:)
|
|
@@ -46,7 +46,7 @@ module RubyCms
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def matching_templates(user)
|
|
49
|
-
user_keys = RubyCms::UserPermission.where(user:
|
|
49
|
+
user_keys = RubyCms::UserPermission.where(user:)
|
|
50
50
|
.joins(:permission)
|
|
51
51
|
.pluck("permissions.key")
|
|
52
52
|
templates.select {|_, tmpl| (tmpl[:keys] - user_keys).empty? }
|
|
@@ -25,10 +25,18 @@ module RubyCms
|
|
|
25
25
|
|
|
26
26
|
def dashboard_stats
|
|
27
27
|
Rails.cache.fetch(cache_key("dashboard"), expires_in: cache_duration) do
|
|
28
|
+
total_views = page_view_events.count
|
|
29
|
+
total_sessions = visits.distinct.count(:visit_token)
|
|
30
|
+
unique_visitors = visits.distinct.count(:visitor_token)
|
|
31
|
+
|
|
28
32
|
{
|
|
29
|
-
total_page_views:
|
|
30
|
-
unique_visitors:
|
|
31
|
-
total_sessions:
|
|
33
|
+
total_page_views: total_views,
|
|
34
|
+
unique_visitors: unique_visitors,
|
|
35
|
+
total_sessions: total_sessions,
|
|
36
|
+
pages_per_session: total_sessions.positive? ? (total_views.to_f / total_sessions).round(1) : 0,
|
|
37
|
+
bounce_rate: compute_bounce_rate,
|
|
38
|
+
new_visitor_percentage: compute_new_visitor_percentage,
|
|
39
|
+
avg_daily_views: days_in_range.positive? ? (total_views.to_f / days_in_range).round(0).to_i : 0,
|
|
32
40
|
popular_pages: popular_pages_data,
|
|
33
41
|
top_visitors: top_visitors_data,
|
|
34
42
|
hourly_activity: hourly_activity_data,
|
|
@@ -339,6 +347,32 @@ module RubyCms
|
|
|
339
347
|
[]
|
|
340
348
|
end
|
|
341
349
|
|
|
350
|
+
def compute_bounce_rate
|
|
351
|
+
total = visits.distinct.count(:visit_token)
|
|
352
|
+
return 0 unless total.positive?
|
|
353
|
+
|
|
354
|
+
event_counts = page_view_events.group(:visit_id).count
|
|
355
|
+
single_page = event_counts.count { |_, c| c == 1 }
|
|
356
|
+
((single_page.to_f / total) * 100).round(1)
|
|
357
|
+
rescue StandardError
|
|
358
|
+
0
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def compute_new_visitor_percentage
|
|
362
|
+
total = visits.distinct.count(:visitor_token)
|
|
363
|
+
return 0 unless total.positive?
|
|
364
|
+
|
|
365
|
+
returning_tokens = Ahoy::Visit
|
|
366
|
+
.where("started_at < ?", @start_date)
|
|
367
|
+
.distinct
|
|
368
|
+
.pluck(:visitor_token)
|
|
369
|
+
|
|
370
|
+
new_count = visits.where.not(visitor_token: returning_tokens).distinct.count(:visitor_token)
|
|
371
|
+
((new_count.to_f / total) * 100).round(0).to_i
|
|
372
|
+
rescue StandardError
|
|
373
|
+
0
|
|
374
|
+
end
|
|
375
|
+
|
|
342
376
|
def days_in_range
|
|
343
377
|
(@end_date.to_date - @start_date.to_date + 1).to_i
|
|
344
378
|
end
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
subtitle: t("ruby_cms.admin.analytics.subtitle", default: "Traffic and engagement overview"),
|
|
4
4
|
content_card: false
|
|
5
5
|
) do %>
|
|
6
|
-
<div class="space-y-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
<div class="space-y-4">
|
|
7
|
+
<%# ── Period selector + date range ── %>
|
|
8
|
+
<div class="flex flex-wrap items-start justify-between gap-4">
|
|
9
|
+
<div class="inline-flex items-center rounded-lg border border-border/60 bg-white p-1 shadow-sm">
|
|
9
10
|
<% [["Day", "day"], ["Week", "week"], ["Month", "month"], ["Year", "year"]].each do |label, value| %>
|
|
10
11
|
<% start_date = case value
|
|
11
12
|
when "day" then Date.current
|
|
@@ -16,143 +17,137 @@
|
|
|
16
17
|
<% active = params[:period] == value || (params[:period].blank? && value == @period.to_s) %>
|
|
17
18
|
<%= link_to label,
|
|
18
19
|
ruby_cms_admin_analytics_path(start_date:, end_date: Date.current, period: value),
|
|
19
|
-
class: "px-3 py-1.5 text-sm font-medium rounded-md transition #{active ? 'bg-
|
|
20
|
+
class: "px-3 py-1.5 text-sm font-medium rounded-md transition-colors #{active ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}" %>
|
|
20
21
|
<% end %>
|
|
21
22
|
</div>
|
|
22
23
|
|
|
23
24
|
<%= form_with url: ruby_cms_admin_analytics_path, method: :get, local: true, class: "flex items-end gap-2" do |f| %>
|
|
24
25
|
<%= f.hidden_field :period, value: @period %>
|
|
25
26
|
<div class="flex flex-col gap-1">
|
|
26
|
-
<span class="text-xs font-medium text-
|
|
27
|
-
<%= f.date_field :start_date, value: @start_date, class: "h-9 rounded-md border border-
|
|
27
|
+
<span class="text-xs font-medium text-muted-foreground">From</span>
|
|
28
|
+
<%= f.date_field :start_date, value: @start_date, class: "h-9 rounded-md border border-border bg-white px-3 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/20" %>
|
|
28
29
|
</div>
|
|
29
30
|
<div class="flex flex-col gap-1">
|
|
30
|
-
<span class="text-xs font-medium text-
|
|
31
|
-
<%= f.date_field :end_date, value: @end_date, class: "h-9 rounded-md border border-
|
|
31
|
+
<span class="text-xs font-medium text-muted-foreground">To</span>
|
|
32
|
+
<%= f.date_field :end_date, value: @end_date, class: "h-9 rounded-md border border-border bg-white px-3 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/20" %>
|
|
32
33
|
</div>
|
|
33
34
|
<%= f.submit t("ruby_cms.admin.analytics.apply_filters", default: "Apply"),
|
|
34
|
-
class: "h-9 inline-flex items-center justify-center rounded-md bg-
|
|
35
|
+
class: "h-9 inline-flex items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors cursor-pointer" %>
|
|
35
36
|
<% end %>
|
|
36
37
|
</div>
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<
|
|
39
|
+
<%# ── Stat cards ── %>
|
|
40
|
+
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2">
|
|
41
|
+
<div class="rounded-lg border border-border/60 bg-white p-2 shadow-sm">
|
|
42
|
+
<div class="flex items-center justify-between">
|
|
43
|
+
<p class="text-sm font-medium text-muted-foreground"><%= t("ruby_cms.admin.analytics.total_page_views", default: "Page Views") %></p>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="mt-1 flex items-baseline gap-2">
|
|
46
|
+
<p class="text-lg font-semibold tracking-tight text-foreground tabular-nums"><%= number_with_delimiter(@total_page_views) %></p>
|
|
47
|
+
</div>
|
|
48
|
+
<p class="mt-0.5 text-[10px] text-muted-foreground">~<%= number_with_delimiter(@avg_daily_views) %> / day</p>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="rounded-lg border border-border/60 bg-white p-2 shadow-sm">
|
|
52
|
+
<div class="flex items-center justify-between">
|
|
53
|
+
<p class="text-sm font-medium text-muted-foreground"><%= t("ruby_cms.admin.analytics.unique_visitors", default: "Unique Visitors") %></p>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="mt-1 flex items-baseline gap-2">
|
|
56
|
+
<p class="text-lg font-semibold tracking-tight text-foreground tabular-nums"><%= number_with_delimiter(@unique_visitors) %></p>
|
|
57
|
+
<% if @new_visitor_percentage.to_i > 0 %>
|
|
58
|
+
<span class="inline-flex items-center text-xs font-medium text-emerald-600">
|
|
59
|
+
<svg class="w-3 h-3 mr-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18"></path></svg>
|
|
60
|
+
<%= @new_visitor_percentage %>% new
|
|
61
|
+
</span>
|
|
62
|
+
<% end %>
|
|
63
|
+
</div>
|
|
64
|
+
<p class="mt-0.5 text-[10px] text-muted-foreground">Distinct visitor tokens</p>
|
|
42
65
|
</div>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<
|
|
66
|
+
|
|
67
|
+
<div class="rounded-lg border border-border/60 bg-white p-2 shadow-sm">
|
|
68
|
+
<div class="flex items-center justify-between">
|
|
69
|
+
<p class="text-sm font-medium text-muted-foreground"><%= t("ruby_cms.admin.analytics.total_sessions", default: "Sessions") %></p>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="mt-1 flex items-baseline gap-2">
|
|
72
|
+
<p class="text-lg font-semibold tracking-tight text-foreground tabular-nums"><%= number_with_delimiter(@total_sessions) %></p>
|
|
73
|
+
</div>
|
|
74
|
+
<p class="mt-0.5 text-[10px] text-muted-foreground"><%= @pages_per_session %> pages / session</p>
|
|
46
75
|
</div>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<
|
|
76
|
+
|
|
77
|
+
<div class="rounded-lg border border-border/60 bg-white p-2 shadow-sm">
|
|
78
|
+
<div class="flex items-center justify-between">
|
|
79
|
+
<p class="text-sm font-medium text-muted-foreground"><%= t("ruby_cms.admin.analytics.bounce_rate", default: "Bounce Rate") %></p>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="mt-1 flex items-baseline gap-2">
|
|
82
|
+
<p class="text-lg font-semibold tracking-tight text-foreground tabular-nums"><%= @bounce_rate %>%</p>
|
|
83
|
+
<span class="inline-flex items-center text-xs font-medium <%= @bounce_rate.to_f > 70 ? 'text-destructive' : @bounce_rate.to_f > 50 ? 'text-amber-600' : 'text-emerald-600' %>">
|
|
84
|
+
<%= @bounce_rate.to_f > 70 ? "High" : @bounce_rate.to_f > 50 ? "Average" : "Good" %>
|
|
85
|
+
</span>
|
|
86
|
+
</div>
|
|
87
|
+
<p class="mt-0.5 text-[10px] text-muted-foreground">Single-page sessions</p>
|
|
50
88
|
</div>
|
|
51
89
|
</div>
|
|
52
90
|
|
|
53
|
-
|
|
91
|
+
<%# ── Charts ── %>
|
|
92
|
+
<div class="grid grid-cols-1 xl:grid-cols-2 gap-2">
|
|
54
93
|
<%= render "ruby_cms/admin/analytics/partials/daily_activity_chart" %>
|
|
55
94
|
<%= render "ruby_cms/admin/analytics/partials/hourly_activity_chart" %>
|
|
56
95
|
</div>
|
|
57
96
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<p class="text-sm font-semibold text-gray-900"><%= t("ruby_cms.admin.analytics.popular_pages", default: "Popular pages") %></p>
|
|
63
|
-
<p class="text-sm text-gray-500">Most viewed pages in selected range.</p>
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
<% if @popular_pages.present? %>
|
|
68
|
-
<div class="divide-y divide-gray-100">
|
|
69
|
-
<% @popular_pages.first(8).each_with_index do |(page_name, count), idx| %>
|
|
70
|
-
<div class="flex items-center justify-between gap-4 px-6 py-4">
|
|
71
|
-
<div class="min-w-0">
|
|
72
|
-
<p class="text-sm font-medium text-gray-900 truncate"><%= "#{idx + 1}. #{page_name}" %></p>
|
|
73
|
-
<p class="text-sm text-gray-500">Page</p>
|
|
74
|
-
</div>
|
|
75
|
-
<div class="flex items-center gap-3 flex-shrink-0">
|
|
76
|
-
<span class="text-sm font-semibold tabular-nums text-gray-900"><%= number_with_delimiter(count) %></span>
|
|
77
|
-
<%= link_to "Details",
|
|
78
|
-
page_details_ruby_cms_admin_analytics_path(page_name:, start_date: @start_date, end_date: @end_date, period: @period),
|
|
79
|
-
class: "text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors" %>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
<% end %>
|
|
83
|
-
</div>
|
|
84
|
-
<% else %>
|
|
85
|
-
<div class="px-6 py-12 text-center">
|
|
86
|
-
<p class="text-sm font-medium text-gray-900"><%= t("ruby_cms.admin.analytics.no_data", default: "No data yet.") %></p>
|
|
87
|
-
<p class="mt-1 text-sm text-gray-500">Track some traffic to see page analytics.</p>
|
|
88
|
-
</div>
|
|
89
|
-
<% end %>
|
|
97
|
+
<%# ── Content: pages + visitors ── %>
|
|
98
|
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
|
|
99
|
+
<div class="lg:col-span-2">
|
|
100
|
+
<%= render "ruby_cms/admin/analytics/partials/popular_pages" %>
|
|
90
101
|
</div>
|
|
102
|
+
<%= render "ruby_cms/admin/analytics/partials/top_visitors" %>
|
|
103
|
+
</div>
|
|
91
104
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
</div>
|
|
105
|
+
<%# ── Sources & tech ── %>
|
|
106
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
|
107
|
+
<%= render "ruby_cms/admin/analytics/partials/top_referrers" %>
|
|
108
|
+
<%= render "ruby_cms/admin/analytics/partials/landing_pages" %>
|
|
109
|
+
<%= render "ruby_cms/admin/analytics/partials/utm_sources" %>
|
|
110
|
+
</div>
|
|
99
111
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<div class="flex items-center gap-3 flex-shrink-0">
|
|
106
|
-
<span class="text-sm font-semibold tabular-nums text-gray-900"><%= number_with_delimiter(count) %></span>
|
|
107
|
-
<%= link_to "Details",
|
|
108
|
-
visitor_details_ruby_cms_admin_analytics_path(ip_address: ip, start_date: @start_date, end_date: @end_date, period: @period),
|
|
109
|
-
class: "text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors" %>
|
|
110
|
-
</div>
|
|
111
|
-
</div>
|
|
112
|
-
<% end %>
|
|
113
|
-
</div>
|
|
114
|
-
<% else %>
|
|
115
|
-
<div class="px-6 py-12 text-center">
|
|
116
|
-
<p class="text-sm font-medium text-gray-900"><%= t("ruby_cms.admin.analytics.no_data", default: "No data yet.") %></p>
|
|
117
|
-
<p class="mt-1 text-sm text-gray-500">No visitors in selected range.</p>
|
|
118
|
-
</div>
|
|
119
|
-
<% end %>
|
|
120
|
-
</div>
|
|
112
|
+
<%# ── Tech & activity ── %>
|
|
113
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
|
114
|
+
<%= render "ruby_cms/admin/analytics/partials/browser_device" %>
|
|
115
|
+
<%= render "ruby_cms/admin/analytics/partials/os_stats" %>
|
|
116
|
+
<%= render "ruby_cms/admin/analytics/partials/recent_activity" %>
|
|
121
117
|
</div>
|
|
122
118
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
<p class="text-sm text-
|
|
119
|
+
<%# ── Suspicious activity ── %>
|
|
120
|
+
<% if @suspicious_activity.present? %>
|
|
121
|
+
<div class="rounded-lg border border-amber-200 bg-amber-50/50 shadow-sm">
|
|
122
|
+
<div class="px-3 py-2 border-b border-amber-200/60">
|
|
123
|
+
<p class="text-sm font-semibold text-amber-900 flex items-center gap-2">
|
|
124
|
+
<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>
|
|
125
|
+
<%= t("ruby_cms.admin.analytics.suspicious_activity", default: "Suspicious activity") %>
|
|
126
|
+
</p>
|
|
127
|
+
<p class="text-sm text-amber-700/80 mt-0.5">Potential bot-like traffic signals.</p>
|
|
128
128
|
</div>
|
|
129
|
-
<div class="px-
|
|
130
|
-
<%
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<span class="text-sm font-semibold tabular-nums text-gray-900"><%= number_with_delimiter(item[:count]) %></span>
|
|
136
|
-
</li>
|
|
137
|
-
<% end %>
|
|
138
|
-
</ul>
|
|
139
|
-
<% else %>
|
|
140
|
-
<p class="text-sm text-gray-500"><%= t("ruby_cms.admin.analytics.none_detected", default: "None detected in selected range.") %></p>
|
|
129
|
+
<div class="px-3 py-2 divide-y divide-amber-200/40">
|
|
130
|
+
<% @suspicious_activity.first(6).each do |item| %>
|
|
131
|
+
<div class="flex items-center justify-between gap-3 py-2 first:pt-0 last:pb-0">
|
|
132
|
+
<span class="text-sm text-amber-800"><%= item[:description] %></span>
|
|
133
|
+
<span class="text-sm font-semibold tabular-nums text-amber-900"><%= number_with_delimiter(item[:count]) %></span>
|
|
134
|
+
</div>
|
|
141
135
|
<% end %>
|
|
142
136
|
</div>
|
|
143
137
|
</div>
|
|
138
|
+
<% end %>
|
|
144
139
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
140
|
+
<%# ── Extra cards hook ── %>
|
|
141
|
+
<% if Array(@extra_cards).any? %>
|
|
142
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
|
143
|
+
<% @extra_cards.each do |card| %>
|
|
144
|
+
<div class="rounded-lg border border-border/60 bg-white p-3 shadow-sm">
|
|
145
|
+
<p class="text-sm font-medium text-muted-foreground"><%= card[:title].to_s %></p>
|
|
146
|
+
<p class="mt-1 text-xl font-semibold tracking-tight text-foreground"><%= card[:value].to_s %></p>
|
|
147
|
+
<% if card[:description].present? %>
|
|
148
|
+
<p class="mt-1 text-[11px] text-muted-foreground"><%= card[:description] %></p>
|
|
149
|
+
<% end %>
|
|
150
|
+
</div>
|
|
156
151
|
<% end %>
|
|
157
152
|
</div>
|
|
158
153
|
<% end %>
|