ruby_cms 0.2.0.9 → 0.2.1.1
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 +7 -0
- data/README.md +20 -0
- data/app/controllers/ruby_cms/admin/analytics_controller.rb +11 -1
- data/app/controllers/ruby_cms/admin/dashboard_controller.rb +2 -3
- data/app/helpers/ruby_cms/application_helper.rb +8 -0
- data/app/javascript/controllers/ruby_cms/index.js +0 -1
- data/app/views/layouts/ruby_cms/_admin_sidebar.html.erb +171 -53
- data/app/views/ruby_cms/_tailwind_safelist.html.erb +2 -0
- data/app/views/ruby_cms/admin/analytics/index.html.erb +8 -8
- data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +306 -51
- data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +296 -48
- data/app/views/ruby_cms/admin/dashboard/blocks/_analytics_overview.html.erb +63 -36
- data/app/views/ruby_cms/admin/dashboard/blocks/_quick_actions.html.erb +2 -2
- data/app/views/ruby_cms/admin/dashboard/index.html.erb +3 -12
- data/lib/generators/ruby_cms/install_generator.rb +83 -4
- data/lib/ruby_cms/engine/navigation_registration.rb +17 -16
- data/lib/ruby_cms/version.rb +1 -1
- data/lib/ruby_cms.rb +83 -0
- metadata +2 -1
|
@@ -1,76 +1,331 @@
|
|
|
1
|
-
|
|
1
|
+
<%
|
|
2
|
+
chart_id = "rcms-daily-#{SecureRandom.hex(4)}"
|
|
3
|
+
daily_arr = @daily_activity.is_a?(Hash) ? @daily_activity.to_a : (@daily_activity || [])
|
|
4
|
+
has_data = daily_arr.present? && daily_arr.any?
|
|
5
|
+
max_views = daily_arr.map { |_k, v| v.to_i }.max.to_i
|
|
6
|
+
max_visitors = (@daily_visitors || {}).values.map(&:to_i).max.to_i
|
|
7
|
+
max_value = [max_views, max_visitors, 1].max
|
|
8
|
+
# Plot height + x-axis band (month view labels like "03/12–03/14" need ~32–36px; 20px was too tight and clipped)
|
|
9
|
+
bar_area_h = 160
|
|
10
|
+
x_axis_h = 40
|
|
11
|
+
chart_h = bar_area_h + x_axis_h
|
|
12
|
+
total_views = daily_arr.sum { |_k, v| v.to_i }
|
|
13
|
+
total_visitors = (@daily_visitors || {}).values.sum(&:to_i)
|
|
14
|
+
n_days = [daily_arr.size.to_f, 1.0].max
|
|
15
|
+
line_pts_views = daily_arr.each_with_index.map { |(d, vc), i|
|
|
16
|
+
vx = ((i + 0.5) / n_days) * 100.0
|
|
17
|
+
vy = 100.0 - (max_value.positive? ? (vc.to_f / max_value * 100.0) : 0.0)
|
|
18
|
+
"#{vx.round(4)},#{vy.round(4)}"
|
|
19
|
+
}.join(" ")
|
|
20
|
+
line_pts_visitors = daily_arr.each_with_index.map { |(d, _vc), i|
|
|
21
|
+
vc = (@daily_visitors || {})[d].to_i
|
|
22
|
+
vx = ((i + 0.5) / n_days) * 100.0
|
|
23
|
+
vy = 100.0 - (max_value.positive? ? (vc.to_f / max_value * 100.0) : 0.0)
|
|
24
|
+
"#{vx.round(4)},#{vy.round(4)}"
|
|
25
|
+
}.join(" ")
|
|
26
|
+
# Month view: compact day-number ticks + scroll; ~14px per slot is enough when labels are "3".."31"
|
|
27
|
+
plot_scroll_min_w = [daily_arr.size * 14 + 56, 280].max
|
|
28
|
+
%>
|
|
29
|
+
|
|
30
|
+
<div class="min-w-0 rounded-xl border border-border/60 bg-white shadow-sm">
|
|
31
|
+
|
|
32
|
+
<%# ── Header ── %>
|
|
2
33
|
<div class="flex items-center justify-between gap-3 px-4 py-3 border-b border-border/40">
|
|
3
34
|
<div class="flex items-center gap-3">
|
|
4
35
|
<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"
|
|
36
|
+
<svg class="w-4 h-4 text-sky-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
37
|
+
<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"/>
|
|
38
|
+
</svg>
|
|
6
39
|
</div>
|
|
7
40
|
<div>
|
|
8
41
|
<p class="text-sm font-semibold text-foreground"><%= t("ruby_cms.admin.analytics.daily_activity", default: "Daily Activity") %></p>
|
|
9
42
|
<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
43
|
</div>
|
|
11
44
|
</div>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
45
|
+
|
|
46
|
+
<div class="flex items-center gap-3 sm:gap-4 flex-shrink-0 flex-wrap justify-end">
|
|
47
|
+
<% if has_data %>
|
|
48
|
+
<div class="hidden sm:flex items-center gap-3 text-xs text-muted-foreground">
|
|
49
|
+
<span class="tabular-nums"><strong class="text-foreground font-semibold"><%= number_with_delimiter(total_views) %></strong> views</span>
|
|
50
|
+
<span class="tabular-nums"><strong class="text-foreground font-semibold"><%= number_with_delimiter(total_visitors) %></strong> visitors</span>
|
|
51
|
+
</div>
|
|
52
|
+
<% end %>
|
|
53
|
+
<div class="flex items-center gap-3 text-xs text-muted-foreground">
|
|
54
|
+
<span class="inline-flex items-center gap-1.5">
|
|
55
|
+
<span class="inline-block h-2 w-2 shrink-0 rounded-full" style="background:rgb(37 99 235)" aria-hidden="true"></span>
|
|
56
|
+
<%= t("ruby_cms.admin.analytics.views", default: "Views") %>
|
|
57
|
+
</span>
|
|
58
|
+
<span class="inline-flex items-center gap-1.5">
|
|
59
|
+
<span class="inline-block h-2 w-2 shrink-0 rounded-full" style="background:rgb(96 165 250)" aria-hidden="true"></span>
|
|
60
|
+
<%= t("ruby_cms.admin.analytics.visitors", default: "Visitors") %>
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
<% if has_data %>
|
|
64
|
+
<div class="rcms-chart-mode-toggle inline-flex rounded-lg border border-border/60 bg-muted/30 p-0.5 text-[11px] font-medium"
|
|
65
|
+
role="group"
|
|
66
|
+
data-rcms-chart="<%= chart_id %>"
|
|
67
|
+
aria-label="<%= t('ruby_cms.admin.analytics.chart_display', default: 'Chart display') %>">
|
|
68
|
+
<button type="button"
|
|
69
|
+
class="rcms-mode-btn px-2 py-1 rounded-md transition-colors text-muted-foreground hover:text-foreground"
|
|
70
|
+
data-mode="bars"
|
|
71
|
+
aria-pressed="true">
|
|
72
|
+
<%= t("ruby_cms.admin.analytics.chart_bars", default: "Bars") %>
|
|
73
|
+
</button>
|
|
74
|
+
<button type="button"
|
|
75
|
+
class="rcms-mode-btn px-2 py-1 rounded-md transition-colors text-muted-foreground hover:text-foreground"
|
|
76
|
+
data-mode="line"
|
|
77
|
+
aria-pressed="false">
|
|
78
|
+
<%= t("ruby_cms.admin.analytics.chart_line", default: "Line") %>
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
<% end %>
|
|
15
82
|
</div>
|
|
16
83
|
</div>
|
|
17
84
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
%>
|
|
28
|
-
|
|
29
|
-
<div class="grid grid-cols-[2.75rem_1fr] gap-2">
|
|
30
|
-
<div class="flex flex-col justify-between text-xs text-muted-foreground tabular-nums" style="height:<%= chart_h %>px">
|
|
31
|
-
<% [100, 75, 50, 25, 0].each do |pct| %>
|
|
32
|
-
<div class="text-right"><%= number_with_delimiter((max_value * pct / 100.0).round) %></div>
|
|
33
|
-
<% end %>
|
|
85
|
+
<%# ── Chart body ── %>
|
|
86
|
+
<div class="min-w-0 px-4 py-4">
|
|
87
|
+
<% if has_data %>
|
|
88
|
+
<div id="<%= chart_id %>" class="relative min-w-0 select-none">
|
|
89
|
+
|
|
90
|
+
<%# Hover info card %>
|
|
91
|
+
<div id="<%= chart_id %>-tip"
|
|
92
|
+
role="tooltip"
|
|
93
|
+
class="pointer-events-none absolute z-[100] hidden min-w-[10rem] max-w-[16rem]">
|
|
34
94
|
</div>
|
|
35
95
|
|
|
36
|
-
<div class="
|
|
37
|
-
<div class="
|
|
38
|
-
|
|
96
|
+
<div class="w-full min-w-0 overflow-x-auto overflow-y-visible overscroll-x-contain">
|
|
97
|
+
<div class="min-w-0" style="width:max(100%,<%= plot_scroll_min_w %>px)">
|
|
98
|
+
<div class="grid min-w-0 gap-2" style="grid-template-columns:2.75rem minmax(0,1fr);">
|
|
99
|
+
|
|
100
|
+
<%# Y-axis %>
|
|
101
|
+
<div class="flex flex-col justify-between text-right text-xs text-muted-foreground tabular-nums" style="height:<%= chart_h %>px; padding-bottom:<%= x_axis_h %>px;">
|
|
102
|
+
<% [100, 75, 50, 25, 0].each do |pct| %>
|
|
103
|
+
<div class="leading-none"><%= number_with_delimiter((max_value * pct / 100.0).round) %></div>
|
|
104
|
+
<% end %>
|
|
39
105
|
</div>
|
|
40
106
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
107
|
+
<%# Plot area %>
|
|
108
|
+
<div class="relative min-w-0" style="height:<%= chart_h %>px;">
|
|
109
|
+
|
|
110
|
+
<%# Grid lines %>
|
|
111
|
+
<div class="absolute inset-0 flex flex-col justify-between pointer-events-none" style="padding-bottom:<%= x_axis_h %>px;">
|
|
112
|
+
<% 5.times do %><div style="border-top:1px solid hsl(var(--border,214 32% 91%) / 0.3);"></div><% end %>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<%# Line chart (toggled) %>
|
|
116
|
+
<div class="rcms-line-layer absolute left-0 right-0 pointer-events-none z-[1] hidden"
|
|
117
|
+
style="height:<%= bar_area_h %>px; top:0;"
|
|
118
|
+
aria-hidden="true">
|
|
119
|
+
<svg class="w-full h-full overflow-visible" viewBox="0 0 100 100" preserveAspectRatio="none">
|
|
120
|
+
<polyline class="rcms-line-views" fill="none" stroke="rgb(37 99 235)" stroke-width="1.25" vector-effect="non-scaling-stroke" stroke-linejoin="round" stroke-linecap="round" points="<%= line_pts_views %>"/>
|
|
121
|
+
<polyline class="rcms-line-visitors" fill="none" stroke="rgb(96 165 250)" stroke-width="1.25" vector-effect="non-scaling-stroke" stroke-linejoin="round" stroke-linecap="round" points="<%= line_pts_visitors %>"/>
|
|
122
|
+
</svg>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<%# Bars %>
|
|
126
|
+
<div class="rcms-bars-layer flex items-end gap-px relative z-[2]" style="height:<%= chart_h %>px;">
|
|
127
|
+
<% daily_arr.each do |date, views_count| %>
|
|
128
|
+
<%
|
|
129
|
+
visitors_count = (@daily_visitors || {})[date].to_i
|
|
130
|
+
vh = views_count.to_i.positive? ? [((views_count.to_f / max_value) * bar_area_h).round, 2].max : 0
|
|
131
|
+
uh = visitors_count.positive? ? [((visitors_count.to_f / max_value) * bar_area_h).round, 2].max : 0
|
|
132
|
+
label = format_chart_date(date)
|
|
133
|
+
short = format_chart_date_axis(date)
|
|
134
|
+
v_pct = total_views.positive? ? (100.0 * views_count.to_i / total_views) : 0.0
|
|
135
|
+
u_pct = total_visitors.positive? ? (100.0 * visitors_count / total_visitors) : 0.0
|
|
136
|
+
%>
|
|
137
|
+
<div class="rcms-bar-group flex flex-col items-center flex-1 min-w-0 cursor-default"
|
|
138
|
+
data-label="<%= label %>"
|
|
139
|
+
data-views="<%= number_with_delimiter(views_count.to_i) %>"
|
|
140
|
+
data-visitors="<%= number_with_delimiter(visitors_count) %>"
|
|
141
|
+
data-views-pct="<%= sprintf('%.1f', v_pct) %>"
|
|
142
|
+
data-visitors-pct="<%= sprintf('%.1f', u_pct) %>"
|
|
143
|
+
data-max-scale="<%= number_with_delimiter(max_value) %>">
|
|
144
|
+
<%# Bar pair %>
|
|
145
|
+
<div class="w-full flex items-end justify-center gap-px rcms-bar-stack" style="height:<%= bar_area_h %>px;">
|
|
146
|
+
<div class="rcms-bar flex-1 rounded-t-sm"
|
|
147
|
+
style="height:<%= vh %>px; max-width:10px; background:rgb(37 99 235 / 0.88); transition:opacity .12s,filter .12s;"></div>
|
|
148
|
+
<div class="rcms-bar flex-1 rounded-t-sm"
|
|
149
|
+
style="height:<%= uh %>px; max-width:10px; background:rgb(96 165 250 / 0.88); transition:opacity .12s,filter .12s;"></div>
|
|
150
|
+
</div>
|
|
151
|
+
<%# X label (month view uses single day number via format_chart_date_axis) %>
|
|
152
|
+
<div class="text-muted-foreground w-full px-px text-center leading-none rcms-x-label tabular-nums"
|
|
153
|
+
style="font-size:10px; min-height:<%= x_axis_h %>px; max-height:<%= x_axis_h %>px; padding-top:6px;"><%= short %></div>
|
|
64
154
|
</div>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
155
|
+
<% end %>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
68
160
|
</div>
|
|
69
161
|
</div>
|
|
70
162
|
</div>
|
|
163
|
+
|
|
164
|
+
<script>
|
|
165
|
+
(function() {
|
|
166
|
+
var BLUE600 = "rgb(37 99 235)";
|
|
167
|
+
var BLUE400 = "rgb(96 165 250)";
|
|
168
|
+
var MODE_KEY = "ruby_cms.analytics.dailyChartMode";
|
|
169
|
+
|
|
170
|
+
function init() {
|
|
171
|
+
var chart = document.getElementById("<%= chart_id %>");
|
|
172
|
+
if (!chart || chart.__rcmsInit) return;
|
|
173
|
+
chart.__rcmsInit = true;
|
|
174
|
+
|
|
175
|
+
var tip = document.getElementById("<%= chart_id %>-tip");
|
|
176
|
+
var groups = chart.querySelectorAll(".rcms-bar-group");
|
|
177
|
+
var allBars = chart.querySelectorAll(".rcms-bar");
|
|
178
|
+
var lineLayer = chart.querySelector(".rcms-line-layer");
|
|
179
|
+
var barsLayer = chart.querySelector(".rcms-bars-layer");
|
|
180
|
+
var linePolys = lineLayer ? lineLayer.querySelectorAll("polyline") : [];
|
|
181
|
+
var toggles = document.querySelectorAll('.rcms-chart-mode-toggle[data-rcms-chart="<%= chart_id %>"] .rcms-mode-btn');
|
|
182
|
+
|
|
183
|
+
function setMode(mode) {
|
|
184
|
+
var isLine = mode === "line";
|
|
185
|
+
if (lineLayer) lineLayer.classList.toggle("hidden", !isLine);
|
|
186
|
+
if (barsLayer) {
|
|
187
|
+
barsLayer.querySelectorAll(".rcms-bar-stack").forEach(function(el) {
|
|
188
|
+
el.style.visibility = isLine ? "hidden" : "visible";
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
linePolys.forEach(function(p) {
|
|
192
|
+
p.style.opacity = isLine ? "1" : "0";
|
|
193
|
+
});
|
|
194
|
+
toggles.forEach(function(btn) {
|
|
195
|
+
var active = btn.getAttribute("data-mode") === mode;
|
|
196
|
+
btn.setAttribute("aria-pressed", active ? "true" : "false");
|
|
197
|
+
btn.classList.toggle("bg-background", active);
|
|
198
|
+
btn.classList.toggle("text-foreground", active);
|
|
199
|
+
btn.classList.toggle("shadow-sm", active);
|
|
200
|
+
});
|
|
201
|
+
try { localStorage.setItem(MODE_KEY, mode); } catch (e) {}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
var saved = localStorage.getItem(MODE_KEY);
|
|
206
|
+
if (saved === "line" || saved === "bars") setMode(saved);
|
|
207
|
+
else setMode("bars");
|
|
208
|
+
} catch (e) {
|
|
209
|
+
setMode("bars");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
toggles.forEach(function(btn) {
|
|
213
|
+
btn.addEventListener("click", function() {
|
|
214
|
+
setMode(btn.getAttribute("data-mode") || "bars");
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
groups.forEach(function(group) {
|
|
219
|
+
group.addEventListener("mouseenter", function() {
|
|
220
|
+
var activeMode = lineLayer && !lineLayer.classList.contains("hidden") ? "line" : "bars";
|
|
221
|
+
if (activeMode === "bars") {
|
|
222
|
+
allBars.forEach(function(b) { b.style.opacity = "0.35"; b.style.filter = ""; });
|
|
223
|
+
group.querySelectorAll(".rcms-bar").forEach(function(b) {
|
|
224
|
+
b.style.opacity = "1";
|
|
225
|
+
b.style.filter = "brightness(1.08)";
|
|
226
|
+
});
|
|
227
|
+
} else {
|
|
228
|
+
linePolys.forEach(function(p) { p.style.opacity = "0.35"; });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
var label = group.dataset.label || "";
|
|
232
|
+
var views = group.dataset.views || "0";
|
|
233
|
+
var visitors = group.dataset.visitors || "0";
|
|
234
|
+
var vp = group.dataset.viewsPct || "0";
|
|
235
|
+
var up = group.dataset.visitorsPct || "0";
|
|
236
|
+
var scale = group.dataset.maxScale || "";
|
|
237
|
+
|
|
238
|
+
var modeStr = activeMode === "line" ? "Line chart" : "Bar chart";
|
|
239
|
+
tip.innerHTML =
|
|
240
|
+
'<div class="overflow-hidden rounded-xl border border-slate-200/90 bg-white text-xs text-slate-800 shadow-[0_10px_38px_-12px_rgba(15,23,42,0.28)]">' +
|
|
241
|
+
'<div class="border-b border-slate-100 bg-gradient-to-br from-sky-50 via-white to-white px-3 py-2">' +
|
|
242
|
+
'<p class="text-[13px] font-semibold leading-snug tracking-tight text-slate-900">' + escapeHtml(label) + "</p>" +
|
|
243
|
+
'<p class="mt-1 text-[10px] leading-relaxed text-slate-500">' +
|
|
244
|
+
"Scale 0–" + escapeHtml(scale) + " · " + modeStr +
|
|
245
|
+
"</p>" +
|
|
246
|
+
"</div>" +
|
|
247
|
+
'<div class="divide-y divide-slate-100/90 bg-white">' +
|
|
248
|
+
'<div class="flex gap-2.5 px-3 py-2">' +
|
|
249
|
+
'<span class="mt-0.5 h-2 w-2 shrink-0 rounded-full shadow-sm ring-2 ring-white" style="background:' + BLUE600 + ';box-shadow:0 0 0 1px rgba(15,23,42,0.06)"></span>' +
|
|
250
|
+
'<div class="min-w-0 flex-1">' +
|
|
251
|
+
'<p class="text-[10px] font-semibold uppercase tracking-wider text-slate-400">Views</p>' +
|
|
252
|
+
'<p class="mt-0.5 text-sm font-semibold tabular-nums text-slate-900">' + escapeHtml(views) +
|
|
253
|
+
' <span class="text-[11px] font-medium text-slate-400">(' + escapeHtml(vp) + "%)</span></p>" +
|
|
254
|
+
"</div>" +
|
|
255
|
+
"</div>" +
|
|
256
|
+
'<div class="flex gap-2.5 px-3 py-2">' +
|
|
257
|
+
'<span class="mt-0.5 h-2 w-2 shrink-0 rounded-full shadow-sm ring-2 ring-white" style="background:' + BLUE400 + ';box-shadow:0 0 0 1px rgba(15,23,42,0.06)"></span>' +
|
|
258
|
+
'<div class="min-w-0 flex-1">' +
|
|
259
|
+
'<p class="text-[10px] font-semibold uppercase tracking-wider text-slate-400">Visitors</p>' +
|
|
260
|
+
'<p class="mt-0.5 text-sm font-semibold tabular-nums text-slate-900">' + escapeHtml(visitors) +
|
|
261
|
+
' <span class="text-[11px] font-medium text-slate-400">(' + escapeHtml(up) + "%)</span></p>" +
|
|
262
|
+
"</div>" +
|
|
263
|
+
"</div>" +
|
|
264
|
+
"</div>" +
|
|
265
|
+
"</div>";
|
|
266
|
+
|
|
267
|
+
tip.classList.remove("hidden");
|
|
268
|
+
requestAnimationFrame(function() {
|
|
269
|
+
position(group, tip, chart);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
group.addEventListener("mouseleave", function() {
|
|
274
|
+
allBars.forEach(function(b) { b.style.opacity = ""; b.style.filter = ""; });
|
|
275
|
+
var isLine = lineLayer && !lineLayer.classList.contains("hidden");
|
|
276
|
+
linePolys.forEach(function(p) {
|
|
277
|
+
p.style.opacity = isLine ? "1" : "0";
|
|
278
|
+
});
|
|
279
|
+
tip.classList.add("hidden");
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function escapeHtml(s) {
|
|
285
|
+
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function position(group, tip, container) {
|
|
289
|
+
var cr = container.getBoundingClientRect();
|
|
290
|
+
var gr = group.getBoundingClientRect();
|
|
291
|
+
var gap = 6;
|
|
292
|
+
var pad = 4;
|
|
293
|
+
var tw = tip.offsetWidth || 160;
|
|
294
|
+
var th = tip.offsetHeight || 72;
|
|
295
|
+
var cx = gr.left - cr.left + gr.width / 2;
|
|
296
|
+
var top = gr.top - cr.top - th - gap;
|
|
297
|
+
|
|
298
|
+
if (top < pad) {
|
|
299
|
+
top = gr.bottom - cr.top + gap;
|
|
300
|
+
}
|
|
301
|
+
var maxTop = container.clientHeight - th - pad;
|
|
302
|
+
if (top > maxTop) {
|
|
303
|
+
top = Math.max(pad, maxTop);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
var left = Math.round(cx - tw / 2);
|
|
307
|
+
left = Math.max(pad, Math.min(left, container.clientWidth - tw - pad));
|
|
308
|
+
|
|
309
|
+
tip.style.position = "absolute";
|
|
310
|
+
tip.style.left = left + "px";
|
|
311
|
+
tip.style.top = Math.round(top) + "px";
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (document.readyState === "loading") {
|
|
315
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
316
|
+
} else {
|
|
317
|
+
init();
|
|
318
|
+
}
|
|
319
|
+
document.addEventListener("turbo:load", init);
|
|
320
|
+
document.addEventListener("turbo:render", init);
|
|
321
|
+
})();
|
|
322
|
+
</script>
|
|
323
|
+
|
|
71
324
|
<% else %>
|
|
72
325
|
<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"
|
|
326
|
+
<svg class="w-8 h-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
327
|
+
<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"/>
|
|
328
|
+
</svg>
|
|
74
329
|
<p class="text-sm text-muted-foreground"><%= t("ruby_cms.admin.analytics.no_daily_data", default: "No daily activity data") %></p>
|
|
75
330
|
</div>
|
|
76
331
|
<% end %>
|