ruby_cms 0.1.0
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 +7 -0
- data/.cursor/dhh.mdc +698 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/README.md +235 -0
- data/Rakefile +30 -0
- data/app/components/ruby_cms/admin/admin_page/admin_table_content.rb +32 -0
- data/app/components/ruby_cms/admin/admin_page.rb +345 -0
- data/app/components/ruby_cms/admin/base_component.rb +78 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +149 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +127 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_body.rb +15 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_cell.rb +41 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_head.rb +33 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +174 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header.rb +59 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +159 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_pagination.rb +192 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_row.rb +97 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +137 -0
- data/app/controllers/concerns/ruby_cms/admin_pagination.rb +120 -0
- data/app/controllers/concerns/ruby_cms/admin_turbo_table.rb +68 -0
- data/app/controllers/concerns/ruby_cms/page_tracking.rb +52 -0
- data/app/controllers/concerns/ruby_cms/visitor_error_capture.rb +39 -0
- data/app/controllers/ruby_cms/admin/analytics_controller.rb +191 -0
- data/app/controllers/ruby_cms/admin/base_controller.rb +105 -0
- data/app/controllers/ruby_cms/admin/content_blocks_controller.rb +390 -0
- data/app/controllers/ruby_cms/admin/dashboard_controller.rb +50 -0
- data/app/controllers/ruby_cms/admin/locale_controller.rb +20 -0
- data/app/controllers/ruby_cms/admin/permissions_controller.rb +66 -0
- data/app/controllers/ruby_cms/admin/settings_controller.rb +223 -0
- data/app/controllers/ruby_cms/admin/user_permissions_controller.rb +59 -0
- data/app/controllers/ruby_cms/admin/users_controller.rb +107 -0
- data/app/controllers/ruby_cms/admin/visitor_errors_controller.rb +89 -0
- data/app/controllers/ruby_cms/admin/visual_editor_controller.rb +322 -0
- data/app/controllers/ruby_cms/errors_controller.rb +35 -0
- data/app/helpers/ruby_cms/admin/admin_page_helper.rb +21 -0
- data/app/helpers/ruby_cms/admin/bulk_action_table_helper.rb +159 -0
- data/app/helpers/ruby_cms/application_helper.rb +41 -0
- data/app/helpers/ruby_cms/bulk_action_table_helper.rb +151 -0
- data/app/helpers/ruby_cms/content_blocks_helper.rb +375 -0
- data/app/helpers/ruby_cms/settings_helper.rb +160 -0
- data/app/javascript/controllers/ruby_cms/auto_save_preference_controller.js +73 -0
- data/app/javascript/controllers/ruby_cms/bulk_action_table_controller.js +553 -0
- data/app/javascript/controllers/ruby_cms/clickable_row_controller.js +28 -0
- data/app/javascript/controllers/ruby_cms/flash_messages_controller.js +29 -0
- data/app/javascript/controllers/ruby_cms/index.js +104 -0
- data/app/javascript/controllers/ruby_cms/locale_tabs_controller.js +34 -0
- data/app/javascript/controllers/ruby_cms/mobile_menu_controller.js +55 -0
- data/app/javascript/controllers/ruby_cms/nav_order_sortable_controller.js +192 -0
- data/app/javascript/controllers/ruby_cms/page_preview_controller.js +135 -0
- data/app/javascript/controllers/ruby_cms/toggle_controller.js +39 -0
- data/app/javascript/controllers/ruby_cms/visual_editor_controller.js +321 -0
- data/app/models/concerns/content_block/publishable.rb +54 -0
- data/app/models/concerns/content_block/searchable.rb +22 -0
- data/app/models/content_block.rb +155 -0
- data/app/models/ruby_cms/content_block.rb +8 -0
- data/app/models/ruby_cms/permission.rb +28 -0
- data/app/models/ruby_cms/permittable.rb +39 -0
- data/app/models/ruby_cms/preference.rb +111 -0
- data/app/models/ruby_cms/user_permission.rb +12 -0
- data/app/models/ruby_cms/visitor_error.rb +109 -0
- data/app/services/ruby_cms/analytics/report.rb +362 -0
- data/app/services/ruby_cms/security_tracker.rb +92 -0
- data/app/views/layouts/ruby_cms/_admin_flash_messages.html.erb +37 -0
- data/app/views/layouts/ruby_cms/_admin_sidebar.html.erb +121 -0
- data/app/views/layouts/ruby_cms/admin.html.erb +81 -0
- data/app/views/layouts/ruby_cms/minimal.html.erb +181 -0
- data/app/views/ruby_cms/admin/analytics/index.html.erb +160 -0
- data/app/views/ruby_cms/admin/analytics/page_details.html.erb +84 -0
- data/app/views/ruby_cms/admin/analytics/partials/_back_button.html.erb +3 -0
- data/app/views/ruby_cms/admin/analytics/partials/_browser_device.html.erb +40 -0
- data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +58 -0
- data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +51 -0
- data/app/views/ruby_cms/admin/analytics/partials/_recent_activity.html.erb +31 -0
- data/app/views/ruby_cms/admin/analytics/partials/_security_alert.html.erb +4 -0
- data/app/views/ruby_cms/admin/analytics/partials/_top_referrers.html.erb +21 -0
- data/app/views/ruby_cms/admin/analytics/visitor_details.html.erb +125 -0
- data/app/views/ruby_cms/admin/content_blocks/_form.html.erb +161 -0
- data/app/views/ruby_cms/admin/content_blocks/_row.html.erb +25 -0
- data/app/views/ruby_cms/admin/content_blocks/edit.html.erb +17 -0
- data/app/views/ruby_cms/admin/content_blocks/index.html.erb +66 -0
- data/app/views/ruby_cms/admin/content_blocks/new.html.erb +5 -0
- data/app/views/ruby_cms/admin/content_blocks/show.html.erb +110 -0
- data/app/views/ruby_cms/admin/dashboard/index.html.erb +198 -0
- data/app/views/ruby_cms/admin/permissions/_row.html.erb +11 -0
- data/app/views/ruby_cms/admin/permissions/index.html.erb +62 -0
- data/app/views/ruby_cms/admin/settings/index.html.erb +220 -0
- data/app/views/ruby_cms/admin/shared/_bulk_action_table_index.html.erb +56 -0
- data/app/views/ruby_cms/admin/user_permissions/index.html.erb +55 -0
- data/app/views/ruby_cms/admin/users/_row.html.erb +14 -0
- data/app/views/ruby_cms/admin/users/index.html.erb +70 -0
- data/app/views/ruby_cms/admin/visitor_errors/_row.html.erb +35 -0
- data/app/views/ruby_cms/admin/visitor_errors/index.html.erb +57 -0
- data/app/views/ruby_cms/admin/visitor_errors/show.html.erb +147 -0
- data/app/views/ruby_cms/admin/visual_editor/index.html.erb +144 -0
- data/app/views/ruby_cms/errors/not_found.html.erb +92 -0
- data/config/database.yml +6 -0
- data/config/importmap.rb +36 -0
- data/config/locales/en.yml +101 -0
- data/config/routes.rb +65 -0
- data/db/migrate/20260125000001_create_ruby_cms_permissions.rb +14 -0
- data/db/migrate/20260125000002_create_ruby_cms_user_permissions.rb +14 -0
- data/db/migrate/20260125000003_create_ruby_cms_content_blocks.rb +19 -0
- data/db/migrate/20260125000010_add_indexes_to_ruby_cms_tables.rb +9 -0
- data/db/migrate/20260127000001_add_locale_to_ruby_cms_content_blocks.rb +34 -0
- data/db/migrate/20260129000001_create_ruby_cms_visitor_errors.rb +24 -0
- data/db/migrate/20260130000001_add_referer_and_query_to_ruby_cms_visitor_errors.rb +8 -0
- data/db/migrate/20260130000002_create_ruby_cms_preferences.rb +16 -0
- data/db/migrate/20260130000003_add_category_to_ruby_cms_preferences.rb +8 -0
- data/db/migrate/20260211000001_add_ruby_cms_analytics_fields_to_ahoy_events.rb +19 -0
- data/db/migrate/20260212000001_use_unprefixed_cms_tables.rb +146 -0
- data/exe/ruby_cms +25 -0
- data/lib/generators/ruby_cms/install_generator.rb +1062 -0
- data/lib/generators/ruby_cms/templates/admin.html.erb +82 -0
- data/lib/generators/ruby_cms/templates/ruby_cms.rb +86 -0
- data/lib/ruby_cms/app_integration.rb +82 -0
- data/lib/ruby_cms/cli.rb +169 -0
- data/lib/ruby_cms/content_blocks_grouping.rb +41 -0
- data/lib/ruby_cms/content_blocks_sync.rb +329 -0
- data/lib/ruby_cms/css_compiler.rb +35 -0
- data/lib/ruby_cms/engine.rb +498 -0
- data/lib/ruby_cms/settings.rb +145 -0
- data/lib/ruby_cms/settings_registry.rb +289 -0
- data/lib/ruby_cms/version.rb +5 -0
- data/lib/ruby_cms.rb +195 -0
- data/lib/tasks/ruby_cms.rake +27 -0
- data/log/test.log +17875 -0
- data/sig/ruby_cms.rbs +4 -0
- metadata +223 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<%# Flash messages with auto-dismiss %>
|
|
2
|
+
<% if flash.any? %>
|
|
3
|
+
<div class="mb-4 space-y-2" data-controller="ruby-cms--flash-messages">
|
|
4
|
+
<% flash.each do |type, message| %>
|
|
5
|
+
<%
|
|
6
|
+
variant =
|
|
7
|
+
case type.to_s
|
|
8
|
+
when "notice"
|
|
9
|
+
"border-emerald-200 bg-emerald-50 text-emerald-900"
|
|
10
|
+
when "alert", "error"
|
|
11
|
+
"border-rose-200 bg-rose-50 text-rose-900"
|
|
12
|
+
else
|
|
13
|
+
"border-gray-200 bg-white text-gray-900"
|
|
14
|
+
end
|
|
15
|
+
%>
|
|
16
|
+
<div class="flash-message px-4 py-3 rounded-lg border flex items-center justify-between shadow-sm transition duration-200 ease-in-out transform <%= variant %>" role="alert" data-ruby-cms--flash-messages-target="message">
|
|
17
|
+
<div class="flex items-center">
|
|
18
|
+
<% if type == "notice" %>
|
|
19
|
+
<svg class="w-5 h-5 text-green-600 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
20
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
|
|
21
|
+
</svg>
|
|
22
|
+
<% elsif type == "alert" %>
|
|
23
|
+
<svg class="w-5 h-5 text-amber-600 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
24
|
+
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
|
|
25
|
+
</svg>
|
|
26
|
+
<% end %>
|
|
27
|
+
<span><%= message %></span>
|
|
28
|
+
</div>
|
|
29
|
+
<button type="button" class="ml-4 text-gray-500 hover:text-gray-900" aria-label="Dismiss" data-action="click->ruby-cms--flash-messages#dismiss">
|
|
30
|
+
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
31
|
+
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
|
32
|
+
</svg>
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
<% end %>
|
|
36
|
+
</div>
|
|
37
|
+
<% end %>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<%# Admin Sidebar Navigation %>
|
|
2
|
+
<aside class="w-56 flex-shrink-0 bg-[#FAF9F5] flex flex-col overflow-hidden" aria-label="Admin" data-ruby-cms--mobile-menu-target="sidebar">
|
|
3
|
+
<div class="px-5 py-5 flex-shrink-0 bg-[#FAF9F5]">
|
|
4
|
+
<div class="flex items-center gap-3" data-ruby-cms--mobile-menu-target="sidebarContent">
|
|
5
|
+
<% if respond_to?(:image_tag, true) %>
|
|
6
|
+
<div class="w-7 h-7 flex-shrink-0 rounded-sm bg-gray-900 flex items-center justify-center text-white font-bold text-sm">
|
|
7
|
+
<% if respond_to?(:main_app, true) && main_app.respond_to?(:image_path) %>
|
|
8
|
+
<%= image_tag main_app.image_path("logo.png"), alt: "Logo", onerror: "this.style.display='none'", class: "w-full h-full object-contain" rescue nil %>
|
|
9
|
+
<% end %>
|
|
10
|
+
</div>
|
|
11
|
+
<% end %>
|
|
12
|
+
<p class="text-base font-semibold text-gray-900 m-0">RubyCMS</p>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<nav class="flex-1 px-3 py-6 overflow-y-auto" aria-label="Admin" data-ruby-cms--mobile-menu-target="sidebarContent">
|
|
17
|
+
<%
|
|
18
|
+
nav_main = "main"
|
|
19
|
+
nav_bottom = "Settings"
|
|
20
|
+
entries = ruby_cms_nav_entries
|
|
21
|
+
by_section = entries.group_by { |e| s = e[:section].to_s.presence || nav_main; s == "custom" ? nav_bottom : s }
|
|
22
|
+
main_items = by_section[nav_main] || []
|
|
23
|
+
bottom_items = by_section[nav_bottom] || []
|
|
24
|
+
%>
|
|
25
|
+
<%# Main nav: top, scrollable %>
|
|
26
|
+
<div class="flex-1 overflow-y-auto space-y-1 min-h-0">
|
|
27
|
+
<% main_items.each do |e| %>
|
|
28
|
+
<% begin %>
|
|
29
|
+
<% path = e[:path].respond_to?(:call) ? e[:path].call(self) : e[:path] %>
|
|
30
|
+
<% next if path.blank? %>
|
|
31
|
+
<% path_str = path.respond_to?(:to_str) ? path.to_str : path.to_s %>
|
|
32
|
+
<% active = (path_str == "/admin" || path_str == "/admin/") ? request.path.match?(%r{\A/admin/?\z}) : request.path.start_with?(path_str) %>
|
|
33
|
+
<%= link_to path, class: "flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-md mb-0.5 no-underline transition-colors #{ active ? 'text-blue-600 font-semibold' : 'text-gray-600 hover:bg-blue-500 hover:text-white' }" do %>
|
|
34
|
+
<% if e[:icon].present? %>
|
|
35
|
+
<svg class="w-5 h-5 flex-shrink-0 text-current" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
36
|
+
<%= ruby_cms_safe_svg_fragment(e[:icon]) %>
|
|
37
|
+
</svg>
|
|
38
|
+
<% end %>
|
|
39
|
+
<span class="flex-1"><%= e[:label] %></span>
|
|
40
|
+
<% end %>
|
|
41
|
+
<% rescue => ex %>
|
|
42
|
+
<% Rails.logger.error "RubyCMS nav error for #{e[:key]}: #{ex.message}" if defined?(Rails.logger) %>
|
|
43
|
+
<% end %>
|
|
44
|
+
<% end %>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<%# Spacer so bottom section sits above footer %>
|
|
48
|
+
<div class="flex-shrink-0 pt-4 mt-4">
|
|
49
|
+
<% if bottom_items.any? %>
|
|
50
|
+
<div class="text-xs font-semibold uppercase tracking-wider text-gray-500 px-2 pb-2">
|
|
51
|
+
<%= t("ruby_cms.nav.settings", default: "Settings") %>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="space-y-1">
|
|
54
|
+
<% bottom_items.each do |e| %>
|
|
55
|
+
<% begin %>
|
|
56
|
+
<% path = e[:path].respond_to?(:call) ? e[:path].call(self) : e[:path] %>
|
|
57
|
+
<% next if path.blank? %>
|
|
58
|
+
<% path_str = path.respond_to?(:to_str) ? path.to_str : path.to_s %>
|
|
59
|
+
<% active = request.path.start_with?(path_str) %>
|
|
60
|
+
<%= link_to path, class: "flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-md mb-0.5 no-underline transition-colors #{ active ? 'text-blue-600 font-semibold' : 'text-gray-600 hover:bg-blue-500 hover:text-white' }" do %>
|
|
61
|
+
<% if e[:icon].present? %>
|
|
62
|
+
<svg class="w-5 h-5 flex-shrink-0 text-current" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
63
|
+
<%= ruby_cms_safe_svg_fragment(e[:icon]) %>
|
|
64
|
+
</svg>
|
|
65
|
+
<% end %>
|
|
66
|
+
<span class="flex-1"><%= e[:label] %></span>
|
|
67
|
+
<% end %>
|
|
68
|
+
<% rescue => ex %>
|
|
69
|
+
<% Rails.logger.error "RubyCMS nav error for #{e[:key]}: #{ex.message}" if defined?(Rails.logger) %>
|
|
70
|
+
<% end %>
|
|
71
|
+
<% end %>
|
|
72
|
+
</div>
|
|
73
|
+
<% end %>
|
|
74
|
+
</div>
|
|
75
|
+
</nav>
|
|
76
|
+
|
|
77
|
+
<div class="px-3 py-3 flex-shrink-0 bg-[#FAF9F5]" data-ruby-cms--mobile-menu-target="sidebarContent">
|
|
78
|
+
<div class="mb-2">
|
|
79
|
+
<%= form_with url: ruby_cms_admin_locale_path, method: :patch, scope: nil, data: { turbo: true } do |form| %>
|
|
80
|
+
<div class="relative">
|
|
81
|
+
<div class="flex items-center gap-3 w-full px-3 py-2.5 text-sm font-medium text-gray-600 rounded-md no-underline transition-colors hover:bg-blue-500 hover:text-white">
|
|
82
|
+
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
83
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 21a9 9 0 100-18 9 9 0 000 18zM3.6 9h16.8M3.6 15h16.8M11 3c-2.2 2.2-3.5 5.2-3.5 9s1.3 6.8 3.5 9m2 0c2.2-2.2 3.5-5.2 3.5-9S15.2 5.2 13 3"/>
|
|
84
|
+
</svg>
|
|
85
|
+
<span class="flex-1 text-left"><%= ruby_cms_locale_display_name(I18n.locale) %></span>
|
|
86
|
+
<svg class="w-4 h-4 flex-shrink-0 opacity-80" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
87
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10l4-4 4 4M16 14l-4 4-4-4"/>
|
|
88
|
+
</svg>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<%= form.select :locale,
|
|
92
|
+
options_for_select(I18n.available_locales.map { |loc| [ruby_cms_locale_display_name(loc), loc.to_s] }, I18n.locale.to_s),
|
|
93
|
+
{},
|
|
94
|
+
{
|
|
95
|
+
class: "absolute inset-0 w-full h-full opacity-0 cursor-pointer",
|
|
96
|
+
aria: { label: "Locale" },
|
|
97
|
+
onchange: "this.form.requestSubmit()"
|
|
98
|
+
} %>
|
|
99
|
+
</div>
|
|
100
|
+
<% end %>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<% if respond_to?(:main_app, true) && main_app.respond_to?(:root_path) %>
|
|
104
|
+
<%= link_to main_app.root_path, target: "_blank", class: "flex items-center gap-3 w-full px-3 py-2.5 text-sm font-medium text-gray-600 rounded-md no-underline transition-colors hover:bg-blue-500 hover:text-white" do %>
|
|
105
|
+
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
106
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
|
107
|
+
</svg>
|
|
108
|
+
<span class="flex-1 text-left">View site</span>
|
|
109
|
+
<% end %>
|
|
110
|
+
<% end %>
|
|
111
|
+
|
|
112
|
+
<% if respond_to?(:main_app, true) && main_app.respond_to?(:session_path) %>
|
|
113
|
+
<%= button_to main_app.session_path, method: :delete, class: "flex items-center gap-3 w-full px-3 py-2.5 text-sm font-medium text-gray-600 rounded-md bg-transparent border-0 cursor-pointer transition-colors hover:bg-blue-500 hover:text-white", data: { turbo_confirm: "Are you sure you want to logout?" } do %>
|
|
114
|
+
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
115
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
|
|
116
|
+
</svg>
|
|
117
|
+
<span class="flex-1 text-left">Logout</span>
|
|
118
|
+
<% end %>
|
|
119
|
+
<% end %>
|
|
120
|
+
</div>
|
|
121
|
+
</aside>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<%# Single admin layout for app and RubyCMS. Uses RubyCMS sidebar, flash, content_for. %>
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html lang="<%= I18n.locale %>">
|
|
4
|
+
<head>
|
|
5
|
+
<title><%= content_for(:title) || "RubyCMS Admin" %></title>
|
|
6
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
7
|
+
<meta name="rails-env" content="<%= Rails.env %>">
|
|
8
|
+
<%= csrf_meta_tags %>
|
|
9
|
+
<%= csp_meta_tag %>
|
|
10
|
+
|
|
11
|
+
<%= favicon_link_tag asset_path("favicon.ico") if respond_to?(:favicon_link_tag, true) %>
|
|
12
|
+
<link rel="icon" type="image/png" sizes="32x32" href="<%= asset_path("favicon-32x32.png") %>">
|
|
13
|
+
<link rel="icon" type="image/png" sizes="16x16" href="<%= asset_path("favicon-16x16.png") %>">
|
|
14
|
+
<link rel="apple-touch-icon" href="<%= asset_path("apple-touch-icon.png") %>">
|
|
15
|
+
<link rel="manifest" href="<%= asset_path("site.webmanifest") %>">
|
|
16
|
+
|
|
17
|
+
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
|
18
|
+
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
|
|
19
|
+
<%= stylesheet_link_tag "actiontext", "data-turbo-track": "reload" if asset_path("actiontext.css") rescue nil %>
|
|
20
|
+
<%= javascript_importmap_tags "admin" %>
|
|
21
|
+
</head>
|
|
22
|
+
<body class="min-h-screen bg-gray-50 text-gray-900">
|
|
23
|
+
<div class="flex h-screen overflow-hidden" data-controller="ruby-cms--mobile-menu">
|
|
24
|
+
<%# Mobile menu toggle (visible on md and below) %>
|
|
25
|
+
<button type="button" class="md:hidden fixed top-4 left-4 z-50 inline-flex items-center justify-center p-2 bg-white border border-gray-200/80 rounded-lg shadow-sm cursor-pointer" aria-label="Toggle menu" data-action="click->ruby-cms--mobile-menu#toggle">
|
|
26
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
27
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
|
28
|
+
</svg>
|
|
29
|
+
</button>
|
|
30
|
+
|
|
31
|
+
<%= render "layouts/ruby_cms/admin_sidebar" %>
|
|
32
|
+
|
|
33
|
+
<%# Mobile overlay (JS adds class "show") %>
|
|
34
|
+
<div class="hidden fixed inset-0 bg-black/50 z-40 lg:hidden" data-ruby-cms--mobile-menu-target="overlay" data-action="click->ruby-cms--mobile-menu#close"></div>
|
|
35
|
+
|
|
36
|
+
<main class="bg-gray-50 flex-1 flex flex-col overflow-hidden min-w-0" role="main">
|
|
37
|
+
<% if content_for?(:full_width) %>
|
|
38
|
+
<%= yield %>
|
|
39
|
+
<% else %>
|
|
40
|
+
<div class="flex-1 flex flex-col min-h-0 p-6 overflow-y-auto bg-gray-50">
|
|
41
|
+
<div class="mx-auto w-full max-w-7xl">
|
|
42
|
+
<% if content_for?(:title) || content_for?(:header_actions) %>
|
|
43
|
+
<header class="flex-shrink-0 mb-4 <%= yield(:header_styles) if content_for?(:header_styles) %>">
|
|
44
|
+
<div class="flex flex-wrap items-center justify-between gap-4">
|
|
45
|
+
<div>
|
|
46
|
+
<% if content_for?(:title) %>
|
|
47
|
+
<h1 class="text-lg font-semibold tracking-tight text-gray-900"><%= yield :title %></h1>
|
|
48
|
+
<% end %>
|
|
49
|
+
<% if content_for?(:breadcrumbs) %>
|
|
50
|
+
<nav class="mt-0.5 text-sm text-gray-500" aria-label="Breadcrumb">
|
|
51
|
+
<ol class="flex items-center flex-wrap gap-x-2 gap-y-1">
|
|
52
|
+
<%= yield :breadcrumbs %>
|
|
53
|
+
</ol>
|
|
54
|
+
</nav>
|
|
55
|
+
<% end %>
|
|
56
|
+
</div>
|
|
57
|
+
<% if content_for?(:header_actions) %>
|
|
58
|
+
<div class="flex items-center flex-shrink-0">
|
|
59
|
+
<%= yield :header_actions %>
|
|
60
|
+
</div>
|
|
61
|
+
<% end %>
|
|
62
|
+
</div>
|
|
63
|
+
</header>
|
|
64
|
+
<% elsif content_for?(:breadcrumbs) %>
|
|
65
|
+
<nav class="mb-4" aria-label="Breadcrumb">
|
|
66
|
+
<ol class="flex items-center space-x-2 text-sm text-gray-600">
|
|
67
|
+
<%= content_for :breadcrumbs %>
|
|
68
|
+
</ol>
|
|
69
|
+
</nav>
|
|
70
|
+
<% end %>
|
|
71
|
+
|
|
72
|
+
<%= render "layouts/ruby_cms/admin_flash_messages" %>
|
|
73
|
+
|
|
74
|
+
<%= yield %>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<% end %>
|
|
78
|
+
</main>
|
|
79
|
+
</div>
|
|
80
|
+
</body>
|
|
81
|
+
</html>
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="<%= I18n.locale %>">
|
|
3
|
+
<head>
|
|
4
|
+
<title><%= content_for(:title) || "Page Preview" %></title>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<meta name="rails-env" content="<%= Rails.env %>">
|
|
7
|
+
<meta name="locale" content="<%= I18n.locale %>">
|
|
8
|
+
<%= csrf_meta_tags %>
|
|
9
|
+
<%= csp_meta_tag %>
|
|
10
|
+
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
|
11
|
+
<%# Conditionally include tailwind stylesheet if it exists (works with both Sprockets and Propshaft) %>
|
|
12
|
+
<% tailwind_exists = begin
|
|
13
|
+
if Rails.application.assets.respond_to?(:find_asset)
|
|
14
|
+
# Sprockets
|
|
15
|
+
Rails.application.assets.find_asset("tailwind")
|
|
16
|
+
else
|
|
17
|
+
# Propshaft or other - assume it exists, Rails will handle missing assets gracefully
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
rescue
|
|
21
|
+
false
|
|
22
|
+
end %>
|
|
23
|
+
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" if tailwind_exists %>
|
|
24
|
+
<%= javascript_importmap_tags if respond_to?(:javascript_importmap_tags) %>
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
/* Content block interaction styles */
|
|
28
|
+
.ruby_cms-preview-page {
|
|
29
|
+
position: relative;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.ruby_cms-preview-page--edit-mode .ruby_cms-content-block {
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
transition: all 0.2s ease;
|
|
35
|
+
position: relative;
|
|
36
|
+
display: inline-block;
|
|
37
|
+
min-height: 1em;
|
|
38
|
+
min-width: 1em;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.ruby_cms-preview-page--edit-mode .content-block {
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
transition: all 0.2s ease;
|
|
44
|
+
position: relative;
|
|
45
|
+
display: inline-block;
|
|
46
|
+
min-height: 1em;
|
|
47
|
+
min-width: 1em;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.ruby_cms-preview-page--edit-mode .ruby_cms-content-block:hover {
|
|
51
|
+
outline: 2px dashed rgb(59 130 246);
|
|
52
|
+
outline-offset: 2px;
|
|
53
|
+
background-color: rgba(59, 130, 246, 0.05);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.ruby_cms-preview-page--edit-mode .content-block:hover {
|
|
57
|
+
outline: 2px dashed rgb(59 130 246);
|
|
58
|
+
outline-offset: 2px;
|
|
59
|
+
background-color: rgba(59, 130, 246, 0.05);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.ruby_cms-content-block.editing {
|
|
63
|
+
outline: 2px solid rgb(34 197 94);
|
|
64
|
+
outline-offset: 2px;
|
|
65
|
+
background-color: rgba(34, 197, 94, 0.1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.content-block.editing {
|
|
69
|
+
outline: 2px solid rgb(34 197 94);
|
|
70
|
+
outline-offset: 2px;
|
|
71
|
+
background-color: rgba(34, 197, 94, 0.1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Make block-level content blocks more visible */
|
|
75
|
+
.ruby_cms-preview-page--edit-mode .ruby_cms-content-block[data-block-id],
|
|
76
|
+
.ruby_cms-preview-page--edit-mode .ruby_cms-content-block[data-content-key] {
|
|
77
|
+
display: block;
|
|
78
|
+
padding: 0.25rem;
|
|
79
|
+
margin: 0.125rem 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.ruby_cms-preview-page--edit-mode .content-block[data-block-id],
|
|
83
|
+
.ruby_cms-preview-page--edit-mode .content-block[data-content-key] {
|
|
84
|
+
display: block;
|
|
85
|
+
padding: 0.25rem;
|
|
86
|
+
margin: 0.125rem 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.ruby_cms-preview-page--edit-mode .ruby_cms-content-block[data-block-id]:hover::before,
|
|
90
|
+
.ruby_cms-preview-page--edit-mode .ruby_cms-content-block[data-content-key]:hover::before {
|
|
91
|
+
content: "✎ Edit";
|
|
92
|
+
position: absolute;
|
|
93
|
+
top: -1.5rem;
|
|
94
|
+
left: 0;
|
|
95
|
+
font-size: 0.75rem;
|
|
96
|
+
color: rgb(59 130 246);
|
|
97
|
+
background: white;
|
|
98
|
+
padding: 0.125rem 0.25rem;
|
|
99
|
+
border-radius: 0.25rem;
|
|
100
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
101
|
+
pointer-events: none;
|
|
102
|
+
z-index: 10;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.ruby_cms-preview-page--edit-mode .content-block[data-block-id]:hover::before,
|
|
106
|
+
.ruby_cms-preview-page--edit-mode .content-block[data-content-key]:hover::before {
|
|
107
|
+
content: "✎ Edit";
|
|
108
|
+
position: absolute;
|
|
109
|
+
top: -1.5rem;
|
|
110
|
+
left: 0;
|
|
111
|
+
font-size: 0.75rem;
|
|
112
|
+
color: rgb(59 130 246);
|
|
113
|
+
background: white;
|
|
114
|
+
padding: 0.125rem 0.25rem;
|
|
115
|
+
border-radius: 0.25rem;
|
|
116
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
117
|
+
pointer-events: none;
|
|
118
|
+
z-index: 10;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.ruby_cms-preview-page__edit-indicator {
|
|
122
|
+
position: fixed;
|
|
123
|
+
top: 1rem;
|
|
124
|
+
right: 1rem;
|
|
125
|
+
z-index: 50;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.ruby_cms-preview-page__edit-badge {
|
|
129
|
+
padding-left: 0.75rem;
|
|
130
|
+
padding-right: 0.75rem;
|
|
131
|
+
padding-top: 0.5rem;
|
|
132
|
+
padding-bottom: 0.5rem;
|
|
133
|
+
background-color: rgb(220 252 231);
|
|
134
|
+
border: 1px solid rgb(187 247 208);
|
|
135
|
+
border-radius: 9999px;
|
|
136
|
+
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
137
|
+
display: flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
gap: 0.5rem;
|
|
140
|
+
font-size: 0.875rem;
|
|
141
|
+
font-weight: 600;
|
|
142
|
+
color: rgb(22 101 52);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.ruby_cms-preview-page__edit-badge-dot {
|
|
146
|
+
width: 0.5rem;
|
|
147
|
+
height: 0.5rem;
|
|
148
|
+
background-color: rgb(34 197 94);
|
|
149
|
+
border-radius: 9999px;
|
|
150
|
+
animation: ruby_cms-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@keyframes ruby_cms-pulse {
|
|
154
|
+
0%, 100% {
|
|
155
|
+
opacity: 1;
|
|
156
|
+
}
|
|
157
|
+
50% {
|
|
158
|
+
opacity: 0.5;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
</style>
|
|
162
|
+
</head>
|
|
163
|
+
<body>
|
|
164
|
+
<div
|
|
165
|
+
class="ruby_cms-preview-page<%= ' ruby_cms-preview-page--edit-mode' if @edit_mode %>"
|
|
166
|
+
data-controller="ruby-cms--page-preview"
|
|
167
|
+
data-ruby-cms--page-preview-edit-mode-value="<%= @edit_mode %>">
|
|
168
|
+
|
|
169
|
+
<% if @edit_mode %>
|
|
170
|
+
<div class="ruby_cms-preview-page__edit-indicator">
|
|
171
|
+
<div class="ruby_cms-preview-page__edit-badge">
|
|
172
|
+
<span class="ruby_cms-preview-page__edit-badge-dot"></span>
|
|
173
|
+
Edit Mode
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
<% end %>
|
|
177
|
+
|
|
178
|
+
<%= yield %>
|
|
179
|
+
</div>
|
|
180
|
+
</body>
|
|
181
|
+
</html>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<%= AdminPage(
|
|
2
|
+
title: t("ruby_cms.admin.analytics.title", default: "Analytics"),
|
|
3
|
+
subtitle: t("ruby_cms.admin.analytics.subtitle", default: "Traffic and engagement overview"),
|
|
4
|
+
content_card: false
|
|
5
|
+
) do %>
|
|
6
|
+
<div class="space-y-6">
|
|
7
|
+
<div class="flex items-start justify-between gap-4">
|
|
8
|
+
<div class="inline-flex items-center rounded-lg bg-gray-100 p-1">
|
|
9
|
+
<% [["Day", "day"], ["Week", "week"], ["Month", "month"], ["Year", "year"]].each do |label, value| %>
|
|
10
|
+
<% start_date = case value
|
|
11
|
+
when "day" then Date.current
|
|
12
|
+
when "week" then 6.days.ago.to_date
|
|
13
|
+
when "month" then 29.days.ago.to_date
|
|
14
|
+
else 364.days.ago.to_date
|
|
15
|
+
end %>
|
|
16
|
+
<% active = params[:period] == value || (params[:period].blank? && value == @period.to_s) %>
|
|
17
|
+
<%= link_to label,
|
|
18
|
+
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-white text-gray-900 shadow-sm' : 'text-gray-600 hover:text-gray-900'}" %>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<%= form_with url: ruby_cms_admin_analytics_path, method: :get, local: true, class: "flex items-end gap-2" do |f| %>
|
|
24
|
+
<%= f.hidden_field :period, value: @period %>
|
|
25
|
+
<div class="flex flex-col gap-1">
|
|
26
|
+
<span class="text-xs font-medium text-gray-500">From</span>
|
|
27
|
+
<%= f.date_field :start_date, value: @start_date, class: "h-9 rounded-md border border-gray-200 bg-white px-3 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-teal-200" %>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="flex flex-col gap-1">
|
|
30
|
+
<span class="text-xs font-medium text-gray-500">To</span>
|
|
31
|
+
<%= f.date_field :end_date, value: @end_date, class: "h-9 rounded-md border border-gray-200 bg-white px-3 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-teal-200" %>
|
|
32
|
+
</div>
|
|
33
|
+
<%= f.submit t("ruby_cms.admin.analytics.apply_filters", default: "Apply"),
|
|
34
|
+
class: "h-9 inline-flex items-center justify-center rounded-md bg-gray-900 px-4 text-sm font-medium text-white shadow-sm hover:bg-gray-800 transition-colors" %>
|
|
35
|
+
<% end %>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="grid grid-cols-3 gap-4">
|
|
39
|
+
<div class="rounded-lg border border-gray-200/80 bg-white p-5 shadow-sm">
|
|
40
|
+
<p class="text-sm font-medium text-gray-500"><%= t("ruby_cms.admin.analytics.total_page_views", default: "Total page views") %></p>
|
|
41
|
+
<p class="mt-2 text-3xl font-semibold tracking-tight text-gray-900 tabular-nums"><%= number_with_delimiter(@total_page_views) %></p>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="rounded-lg border border-gray-200/80 bg-white p-5 shadow-sm">
|
|
44
|
+
<p class="text-sm font-medium text-gray-500"><%= t("ruby_cms.admin.analytics.unique_visitors", default: "Unique visitors") %></p>
|
|
45
|
+
<p class="mt-2 text-3xl font-semibold tracking-tight text-gray-900 tabular-nums"><%= number_with_delimiter(@unique_visitors) %></p>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="rounded-lg border border-gray-200/80 bg-white p-5 shadow-sm">
|
|
48
|
+
<p class="text-sm font-medium text-gray-500"><%= t("ruby_cms.admin.analytics.total_sessions", default: "Total sessions") %></p>
|
|
49
|
+
<p class="mt-2 text-3xl font-semibold tracking-tight text-gray-900 tabular-nums"><%= number_with_delimiter(@total_sessions) %></p>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="grid grid-cols-2 gap-4">
|
|
54
|
+
<%= render "ruby_cms/admin/analytics/partials/daily_activity_chart" %>
|
|
55
|
+
<%= render "ruby_cms/admin/analytics/partials/hourly_activity_chart" %>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="grid grid-cols-3 gap-4">
|
|
59
|
+
<div class="col-span-2 rounded-lg border border-gray-200/80 bg-white shadow-sm">
|
|
60
|
+
<div class="flex items-center justify-between gap-4 px-6 py-4 border-b border-gray-100">
|
|
61
|
+
<div>
|
|
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 %>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="rounded-lg border border-gray-200/80 bg-white shadow-sm">
|
|
93
|
+
<div class="flex items-center justify-between gap-4 px-6 py-4 border-b border-gray-100">
|
|
94
|
+
<div>
|
|
95
|
+
<p class="text-sm font-semibold text-gray-900"><%= t("ruby_cms.admin.analytics.top_visitors", default: "Top visitors") %></p>
|
|
96
|
+
<p class="text-sm text-gray-500">Most active IPs.</p>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<% if @top_visitors.present? %>
|
|
101
|
+
<div class="divide-y divide-gray-100">
|
|
102
|
+
<% @top_visitors.first(6).each_with_index do |(ip, count), idx| %>
|
|
103
|
+
<div class="flex items-center justify-between gap-4 px-6 py-4">
|
|
104
|
+
<p class="text-sm font-medium text-gray-900 tabular-nums truncate"><%= "#{idx + 1}. #{ip}" %></p>
|
|
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>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div class="grid grid-cols-3 gap-4">
|
|
124
|
+
<div class="rounded-lg border border-gray-200/80 bg-white shadow-sm">
|
|
125
|
+
<div class="px-6 py-4 border-b border-gray-100">
|
|
126
|
+
<p class="text-sm font-semibold text-gray-900"><%= t("ruby_cms.admin.analytics.suspicious_activity", default: "Suspicious activity") %></p>
|
|
127
|
+
<p class="text-sm text-gray-500">Potential bot-like traffic signals.</p>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="px-6 py-4">
|
|
130
|
+
<% if @suspicious_activity.present? %>
|
|
131
|
+
<ul class="space-y-2">
|
|
132
|
+
<% @suspicious_activity.first(6).each do |item| %>
|
|
133
|
+
<li class="flex items-center justify-between gap-3">
|
|
134
|
+
<span class="text-sm text-gray-700"><%= item[:description] %></span>
|
|
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>
|
|
141
|
+
<% end %>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<%= render "ruby_cms/admin/analytics/partials/recent_activity" %>
|
|
146
|
+
<%= render "ruby_cms/admin/analytics/partials/top_referrers" %>
|
|
147
|
+
<%= render "ruby_cms/admin/analytics/partials/browser_device" %>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<% Array(@extra_cards).each do |card| %>
|
|
151
|
+
<div class="rounded-lg border border-gray-200/80 bg-white p-6 shadow-sm">
|
|
152
|
+
<p class="text-sm font-semibold text-gray-900"><%= card[:title].to_s %></p>
|
|
153
|
+
<p class="mt-2 text-2xl font-semibold tracking-tight text-gray-900"><%= card[:value].to_s %></p>
|
|
154
|
+
<% if card[:description].present? %>
|
|
155
|
+
<p class="mt-1 text-sm text-gray-500"><%= card[:description] %></p>
|
|
156
|
+
<% end %>
|
|
157
|
+
</div>
|
|
158
|
+
<% end %>
|
|
159
|
+
</div>
|
|
160
|
+
<% end %>
|