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.
Files changed (131) hide show
  1. checksums.yaml +7 -0
  2. data/.cursor/dhh.mdc +698 -0
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +10 -0
  6. data/README.md +235 -0
  7. data/Rakefile +30 -0
  8. data/app/components/ruby_cms/admin/admin_page/admin_table_content.rb +32 -0
  9. data/app/components/ruby_cms/admin/admin_page.rb +345 -0
  10. data/app/components/ruby_cms/admin/base_component.rb +78 -0
  11. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +149 -0
  12. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +127 -0
  13. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_body.rb +15 -0
  14. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_cell.rb +41 -0
  15. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_head.rb +33 -0
  16. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +174 -0
  17. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header.rb +59 -0
  18. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +159 -0
  19. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_pagination.rb +192 -0
  20. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_row.rb +97 -0
  21. data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +137 -0
  22. data/app/controllers/concerns/ruby_cms/admin_pagination.rb +120 -0
  23. data/app/controllers/concerns/ruby_cms/admin_turbo_table.rb +68 -0
  24. data/app/controllers/concerns/ruby_cms/page_tracking.rb +52 -0
  25. data/app/controllers/concerns/ruby_cms/visitor_error_capture.rb +39 -0
  26. data/app/controllers/ruby_cms/admin/analytics_controller.rb +191 -0
  27. data/app/controllers/ruby_cms/admin/base_controller.rb +105 -0
  28. data/app/controllers/ruby_cms/admin/content_blocks_controller.rb +390 -0
  29. data/app/controllers/ruby_cms/admin/dashboard_controller.rb +50 -0
  30. data/app/controllers/ruby_cms/admin/locale_controller.rb +20 -0
  31. data/app/controllers/ruby_cms/admin/permissions_controller.rb +66 -0
  32. data/app/controllers/ruby_cms/admin/settings_controller.rb +223 -0
  33. data/app/controllers/ruby_cms/admin/user_permissions_controller.rb +59 -0
  34. data/app/controllers/ruby_cms/admin/users_controller.rb +107 -0
  35. data/app/controllers/ruby_cms/admin/visitor_errors_controller.rb +89 -0
  36. data/app/controllers/ruby_cms/admin/visual_editor_controller.rb +322 -0
  37. data/app/controllers/ruby_cms/errors_controller.rb +35 -0
  38. data/app/helpers/ruby_cms/admin/admin_page_helper.rb +21 -0
  39. data/app/helpers/ruby_cms/admin/bulk_action_table_helper.rb +159 -0
  40. data/app/helpers/ruby_cms/application_helper.rb +41 -0
  41. data/app/helpers/ruby_cms/bulk_action_table_helper.rb +151 -0
  42. data/app/helpers/ruby_cms/content_blocks_helper.rb +375 -0
  43. data/app/helpers/ruby_cms/settings_helper.rb +160 -0
  44. data/app/javascript/controllers/ruby_cms/auto_save_preference_controller.js +73 -0
  45. data/app/javascript/controllers/ruby_cms/bulk_action_table_controller.js +553 -0
  46. data/app/javascript/controllers/ruby_cms/clickable_row_controller.js +28 -0
  47. data/app/javascript/controllers/ruby_cms/flash_messages_controller.js +29 -0
  48. data/app/javascript/controllers/ruby_cms/index.js +104 -0
  49. data/app/javascript/controllers/ruby_cms/locale_tabs_controller.js +34 -0
  50. data/app/javascript/controllers/ruby_cms/mobile_menu_controller.js +55 -0
  51. data/app/javascript/controllers/ruby_cms/nav_order_sortable_controller.js +192 -0
  52. data/app/javascript/controllers/ruby_cms/page_preview_controller.js +135 -0
  53. data/app/javascript/controllers/ruby_cms/toggle_controller.js +39 -0
  54. data/app/javascript/controllers/ruby_cms/visual_editor_controller.js +321 -0
  55. data/app/models/concerns/content_block/publishable.rb +54 -0
  56. data/app/models/concerns/content_block/searchable.rb +22 -0
  57. data/app/models/content_block.rb +155 -0
  58. data/app/models/ruby_cms/content_block.rb +8 -0
  59. data/app/models/ruby_cms/permission.rb +28 -0
  60. data/app/models/ruby_cms/permittable.rb +39 -0
  61. data/app/models/ruby_cms/preference.rb +111 -0
  62. data/app/models/ruby_cms/user_permission.rb +12 -0
  63. data/app/models/ruby_cms/visitor_error.rb +109 -0
  64. data/app/services/ruby_cms/analytics/report.rb +362 -0
  65. data/app/services/ruby_cms/security_tracker.rb +92 -0
  66. data/app/views/layouts/ruby_cms/_admin_flash_messages.html.erb +37 -0
  67. data/app/views/layouts/ruby_cms/_admin_sidebar.html.erb +121 -0
  68. data/app/views/layouts/ruby_cms/admin.html.erb +81 -0
  69. data/app/views/layouts/ruby_cms/minimal.html.erb +181 -0
  70. data/app/views/ruby_cms/admin/analytics/index.html.erb +160 -0
  71. data/app/views/ruby_cms/admin/analytics/page_details.html.erb +84 -0
  72. data/app/views/ruby_cms/admin/analytics/partials/_back_button.html.erb +3 -0
  73. data/app/views/ruby_cms/admin/analytics/partials/_browser_device.html.erb +40 -0
  74. data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +58 -0
  75. data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +51 -0
  76. data/app/views/ruby_cms/admin/analytics/partials/_recent_activity.html.erb +31 -0
  77. data/app/views/ruby_cms/admin/analytics/partials/_security_alert.html.erb +4 -0
  78. data/app/views/ruby_cms/admin/analytics/partials/_top_referrers.html.erb +21 -0
  79. data/app/views/ruby_cms/admin/analytics/visitor_details.html.erb +125 -0
  80. data/app/views/ruby_cms/admin/content_blocks/_form.html.erb +161 -0
  81. data/app/views/ruby_cms/admin/content_blocks/_row.html.erb +25 -0
  82. data/app/views/ruby_cms/admin/content_blocks/edit.html.erb +17 -0
  83. data/app/views/ruby_cms/admin/content_blocks/index.html.erb +66 -0
  84. data/app/views/ruby_cms/admin/content_blocks/new.html.erb +5 -0
  85. data/app/views/ruby_cms/admin/content_blocks/show.html.erb +110 -0
  86. data/app/views/ruby_cms/admin/dashboard/index.html.erb +198 -0
  87. data/app/views/ruby_cms/admin/permissions/_row.html.erb +11 -0
  88. data/app/views/ruby_cms/admin/permissions/index.html.erb +62 -0
  89. data/app/views/ruby_cms/admin/settings/index.html.erb +220 -0
  90. data/app/views/ruby_cms/admin/shared/_bulk_action_table_index.html.erb +56 -0
  91. data/app/views/ruby_cms/admin/user_permissions/index.html.erb +55 -0
  92. data/app/views/ruby_cms/admin/users/_row.html.erb +14 -0
  93. data/app/views/ruby_cms/admin/users/index.html.erb +70 -0
  94. data/app/views/ruby_cms/admin/visitor_errors/_row.html.erb +35 -0
  95. data/app/views/ruby_cms/admin/visitor_errors/index.html.erb +57 -0
  96. data/app/views/ruby_cms/admin/visitor_errors/show.html.erb +147 -0
  97. data/app/views/ruby_cms/admin/visual_editor/index.html.erb +144 -0
  98. data/app/views/ruby_cms/errors/not_found.html.erb +92 -0
  99. data/config/database.yml +6 -0
  100. data/config/importmap.rb +36 -0
  101. data/config/locales/en.yml +101 -0
  102. data/config/routes.rb +65 -0
  103. data/db/migrate/20260125000001_create_ruby_cms_permissions.rb +14 -0
  104. data/db/migrate/20260125000002_create_ruby_cms_user_permissions.rb +14 -0
  105. data/db/migrate/20260125000003_create_ruby_cms_content_blocks.rb +19 -0
  106. data/db/migrate/20260125000010_add_indexes_to_ruby_cms_tables.rb +9 -0
  107. data/db/migrate/20260127000001_add_locale_to_ruby_cms_content_blocks.rb +34 -0
  108. data/db/migrate/20260129000001_create_ruby_cms_visitor_errors.rb +24 -0
  109. data/db/migrate/20260130000001_add_referer_and_query_to_ruby_cms_visitor_errors.rb +8 -0
  110. data/db/migrate/20260130000002_create_ruby_cms_preferences.rb +16 -0
  111. data/db/migrate/20260130000003_add_category_to_ruby_cms_preferences.rb +8 -0
  112. data/db/migrate/20260211000001_add_ruby_cms_analytics_fields_to_ahoy_events.rb +19 -0
  113. data/db/migrate/20260212000001_use_unprefixed_cms_tables.rb +146 -0
  114. data/exe/ruby_cms +25 -0
  115. data/lib/generators/ruby_cms/install_generator.rb +1062 -0
  116. data/lib/generators/ruby_cms/templates/admin.html.erb +82 -0
  117. data/lib/generators/ruby_cms/templates/ruby_cms.rb +86 -0
  118. data/lib/ruby_cms/app_integration.rb +82 -0
  119. data/lib/ruby_cms/cli.rb +169 -0
  120. data/lib/ruby_cms/content_blocks_grouping.rb +41 -0
  121. data/lib/ruby_cms/content_blocks_sync.rb +329 -0
  122. data/lib/ruby_cms/css_compiler.rb +35 -0
  123. data/lib/ruby_cms/engine.rb +498 -0
  124. data/lib/ruby_cms/settings.rb +145 -0
  125. data/lib/ruby_cms/settings_registry.rb +289 -0
  126. data/lib/ruby_cms/version.rb +5 -0
  127. data/lib/ruby_cms.rb +195 -0
  128. data/lib/tasks/ruby_cms.rake +27 -0
  129. data/log/test.log +17875 -0
  130. data/sig/ruby_cms.rbs +4 -0
  131. metadata +223 -0
@@ -0,0 +1,147 @@
1
+ <div class="rounded-lg border border-gray-200/80 bg-white shadow-sm overflow-hidden">
2
+ <div class="px-6 py-4 border-b border-gray-100">
3
+ <nav class="text-sm text-gray-500 mb-3">
4
+ <%= link_to t("ruby_cms.admin.navigation.admin", default: "Admin"), ruby_cms_admin_root_path, class: "hover:text-gray-700 transition-colors" %> /
5
+ <%= link_to t("ruby_cms.admin.navigation.visitor_errors", default: "Visitor Errors"), ruby_cms_admin_visitor_errors_path, class: "hover:text-gray-700 transition-colors" %> /
6
+ <span><%= t("ruby_cms.admin.visitor_errors.error", default: "Error") %> #<%= @visitor_error.id %></span>
7
+ </nav>
8
+
9
+ <div class="flex flex-wrap items-start justify-between gap-4">
10
+ <h1 class="text-lg font-semibold text-gray-900"><%= t("ruby_cms.admin.visitor_errors.error_details", default: "Error Details") %></h1>
11
+ <div class="flex items-center gap-2">
12
+ <% unless @visitor_error.resolved %>
13
+ <%= button_to resolve_ruby_cms_admin_visitor_error_path(@visitor_error),
14
+ method: :patch,
15
+ class: "inline-flex items-center gap-1.5 h-9 rounded-md bg-emerald-600 px-4 text-sm font-medium text-white shadow-sm hover:bg-emerald-700 transition-colors border-0 cursor-pointer",
16
+ data: { confirm: t("ruby_cms.admin.visitor_errors.mark_resolved_confirm", default: "Mark this error as resolved?") } do %>
17
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
18
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
19
+ </svg>
20
+ <%= t("ruby_cms.admin.visitor_errors.mark_as_resolved", default: "Mark as Resolved") %>
21
+ <% end %>
22
+ <% end %>
23
+
24
+ <%= link_to ruby_cms_admin_visitor_errors_path, class: "inline-flex items-center gap-1.5 h-9 rounded-md border border-gray-200 bg-white px-4 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 transition-colors no-underline" do %>
25
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
26
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
27
+ </svg>
28
+ <%= t("ruby_cms.admin.visitor_errors.back_to_errors", default: "Back to Errors") %>
29
+ <% end %>
30
+ </div>
31
+ </div>
32
+ </div>
33
+
34
+ <div class="flex items-center justify-between px-6 py-4 border-b border-gray-100">
35
+ <h3 class="text-base font-semibold text-gray-900"><%= t("ruby_cms.admin.visitor_errors.error_information", default: "Error Information") %></h3>
36
+ <% if @visitor_error.resolved %>
37
+ <span class="inline-flex items-center gap-1 rounded-full bg-emerald-50 px-3 py-1 text-sm font-medium text-emerald-700">
38
+ <svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
39
+ <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
40
+ </svg>
41
+ <%= t("ruby_cms.admin.visitor_errors.resolved", default: "Resolved") %>
42
+ </span>
43
+ <% else %>
44
+ <span class="inline-flex items-center gap-1 rounded-full bg-rose-50 px-3 py-1 text-sm font-medium text-rose-700">
45
+ <svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
46
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
47
+ </svg>
48
+ <%= t("ruby_cms.admin.visitor_errors.open", default: "Open") %>
49
+ </span>
50
+ <% end %>
51
+ </div>
52
+
53
+ <div class="p-6 space-y-6">
54
+ <div class="grid gap-6" style="grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));">
55
+ <div>
56
+ <label class="block text-xs font-medium uppercase tracking-wide text-gray-400 mb-1.5"><%= t("ruby_cms.admin.visitor_errors.error_id", default: "Error ID") %></label>
57
+ <p class="text-sm font-mono text-gray-900 bg-gray-50 px-3 py-2 rounded-md m-0 break-all"><%= @visitor_error.id %></p>
58
+ </div>
59
+
60
+ <div>
61
+ <label class="block text-xs font-medium uppercase tracking-wide text-gray-400 mb-1.5"><%= t("ruby_cms.admin.visitor_errors.error_class", default: "Error Class") %></label>
62
+ <p class="text-sm font-mono text-gray-900 bg-gray-50 px-3 py-2 rounded-md m-0 break-all"><%= @visitor_error.error_class %></p>
63
+ </div>
64
+
65
+ <div>
66
+ <label class="block text-xs font-medium uppercase tracking-wide text-gray-400 mb-1.5"><%= t("ruby_cms.admin.visitor_errors.request_method", default: "Request Method") %></label>
67
+ <p class="text-sm font-mono text-gray-900 bg-gray-50 px-3 py-2 rounded-md m-0"><%= @visitor_error.request_method %></p>
68
+ </div>
69
+
70
+ <div>
71
+ <label class="block text-xs font-medium uppercase tracking-wide text-gray-400 mb-1.5"><%= t("ruby_cms.admin.visitor_errors.request_path", default: "Request Path") %></label>
72
+ <p class="text-sm font-mono text-gray-900 bg-gray-50 px-3 py-2 rounded-md m-0 break-all"><%= @visitor_error.request_path %></p>
73
+ </div>
74
+
75
+ <div>
76
+ <label class="block text-xs font-medium uppercase tracking-wide text-gray-400 mb-1.5"><%= t("ruby_cms.admin.visitor_errors.ip_address", default: "IP Address") %></label>
77
+ <p class="text-sm font-mono text-gray-900 bg-gray-50 px-3 py-2 rounded-md m-0"><%= @visitor_error.ip_address %></p>
78
+ </div>
79
+
80
+ <div>
81
+ <label class="block text-xs font-medium uppercase tracking-wide text-gray-400 mb-1.5"><%= t("ruby_cms.admin.visitor_errors.browser", default: "Browser") %></label>
82
+ <p class="text-sm font-mono text-gray-900 bg-gray-50 px-3 py-2 rounded-md m-0 break-all"><%= @visitor_error.browser_info %></p>
83
+ </div>
84
+
85
+ <div>
86
+ <label class="block text-xs font-medium uppercase tracking-wide text-gray-400 mb-1.5"><%= t("ruby_cms.admin.visitor_errors.occurred_at", default: "Occurred At") %></label>
87
+ <p class="text-sm font-mono text-gray-900 bg-gray-50 px-3 py-2 rounded-md m-0"><%= @visitor_error.created_at.strftime("%B %d, %Y at %I:%M %p") %></p>
88
+ </div>
89
+
90
+ <div>
91
+ <label class="block text-xs font-medium uppercase tracking-wide text-gray-400 mb-1.5"><%= t("ruby_cms.admin.visitor_errors.last_updated", default: "Last Updated") %></label>
92
+ <p class="text-sm font-mono text-gray-900 bg-gray-50 px-3 py-2 rounded-md m-0"><%= @visitor_error.updated_at.strftime("%B %d, %Y at %I:%M %p") %></p>
93
+ </div>
94
+
95
+ <% if @visitor_error.respond_to?(:referer) && @visitor_error.referer.present? %>
96
+ <div>
97
+ <label class="block text-xs font-medium uppercase tracking-wide text-gray-400 mb-1.5"><%= t("ruby_cms.admin.visitor_errors.referer", default: "Referer") %></label>
98
+ <p class="text-sm font-mono text-gray-900 bg-gray-50 px-3 py-2 rounded-md m-0 break-all"><%= @visitor_error.referer %></p>
99
+ </div>
100
+ <% end %>
101
+
102
+ <% if @visitor_error.respond_to?(:query_string) && @visitor_error.query_string.present? %>
103
+ <div>
104
+ <label class="block text-xs font-medium uppercase tracking-wide text-gray-400 mb-1.5"><%= t("ruby_cms.admin.visitor_errors.query_string", default: "Query String") %></label>
105
+ <p class="text-sm font-mono text-gray-900 bg-gray-50 px-3 py-2 rounded-md m-0 break-all"><%= @visitor_error.query_string %></p>
106
+ </div>
107
+ <% end %>
108
+
109
+ <% if @visitor_error.respond_to?(:session_id) && @visitor_error.session_id.present? %>
110
+ <div>
111
+ <label class="block text-xs font-medium uppercase tracking-wide text-gray-400 mb-1.5"><%= t("ruby_cms.admin.visitor_errors.session_id", default: "Session ID") %></label>
112
+ <p class="text-sm font-mono text-gray-900 bg-gray-50 px-3 py-2 rounded-md m-0 break-all"><%= @visitor_error.session_id %></p>
113
+ </div>
114
+ <% end %>
115
+ </div>
116
+
117
+ <div>
118
+ <label class="block text-xs font-medium uppercase tracking-wide text-gray-400 mb-1.5"><%= t("ruby_cms.admin.visitor_errors.error_message", default: "Error Message") %></label>
119
+ <div class="rounded-md border border-rose-200 bg-rose-50 p-4">
120
+ <p class="text-sm font-mono text-rose-700 m-0"><%= @visitor_error.error_message %></p>
121
+ </div>
122
+ </div>
123
+
124
+ <div>
125
+ <label class="block text-xs font-medium uppercase tracking-wide text-gray-400 mb-1.5"><%= t("ruby_cms.admin.visitor_errors.codepath", default: "Code Path") %></label>
126
+ <div class="rounded-md border border-gray-200 bg-gray-50 p-4 overflow-x-auto">
127
+ <pre class="text-xs font-mono text-gray-900 m-0 whitespace-pre-wrap break-words"><%= @visitor_error.codepath %></pre>
128
+ </div>
129
+ </div>
130
+
131
+ <% if @visitor_error.request_params.present? %>
132
+ <div>
133
+ <label class="block text-xs font-medium uppercase tracking-wide text-gray-400 mb-1.5"><%= t("ruby_cms.admin.visitor_errors.request_parameters", default: "Request Parameters") %></label>
134
+ <div class="rounded-md border border-gray-200 bg-gray-50 p-4 overflow-x-auto">
135
+ <pre class="text-xs font-mono text-gray-900 m-0 whitespace-pre-wrap break-words"><%= JSON.pretty_generate(JSON.parse(@visitor_error.request_params)) rescue @visitor_error.request_params %></pre>
136
+ </div>
137
+ </div>
138
+ <% end %>
139
+
140
+ <% if @visitor_error.user_agent.present? %>
141
+ <div>
142
+ <label class="block text-xs font-medium uppercase tracking-wide text-gray-400 mb-1.5"><%= t("ruby_cms.admin.visitor_errors.user_agent", default: "User Agent") %></label>
143
+ <p class="text-sm font-mono text-gray-900 bg-gray-50 px-3 py-2 rounded-md m-0 break-all"><%= @visitor_error.user_agent %></p>
144
+ </div>
145
+ <% end %>
146
+ </div>
147
+ </div>
@@ -0,0 +1,144 @@
1
+ <% content_for :full_width, true %>
2
+
3
+ <div class="flex-1 flex flex-col min-h-0 p-6 overflow-y-auto">
4
+ <div class="mx-auto w-full max-w-7xl flex flex-col gap-4 flex-1 min-h-0">
5
+ <!-- Header -->
6
+ <div class="flex flex-wrap items-center justify-between gap-4">
7
+ <h1 class="text-lg font-semibold tracking-tight text-gray-900">Visual Editor</h1>
8
+ <%= form_with url: ruby_cms_admin_visual_editor_path, method: :get, class: "flex items-center gap-2", data: { turbo: false } do %>
9
+ <%= hidden_field_tag :edit_mode, @edit_mode %>
10
+
11
+ <% if @available_pages && @available_pages.size > 1 %>
12
+ <%= select_tag(
13
+ :page,
14
+ options_for_select(@available_pages.map { |k, v| [v[:name], k] }, @current_page),
15
+ 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",
16
+ onchange: "this.form.requestSubmit()"
17
+ ) %>
18
+ <% else %>
19
+ <%= hidden_field_tag :page, @current_page %>
20
+ <% end %>
21
+ <% end %>
22
+ </div>
23
+
24
+ <!-- Visual Editor -->
25
+ <div class="rounded-lg border border-gray-200/80 bg-white shadow-sm overflow-hidden flex flex-col flex-1 min-h-0"
26
+ data-controller="ruby-cms--visual-editor"
27
+ data-ruby-cms--visual-editor-current-page-value="<%= @current_page.to_s %>">
28
+
29
+ <!-- Preview iframe -->
30
+ <div class="flex-1 min-h-0 overflow-hidden">
31
+ <iframe
32
+ id="preview-frame"
33
+ class="w-full h-full border-0"
34
+ data-ruby-cms--visual-editor-target="previewFrame"
35
+ src="<%= ruby_cms_admin_visual_editor_page_preview_path(page: @current_page, edit_mode: @edit_mode) %>">
36
+ </iframe>
37
+ </div>
38
+
39
+ <!-- Edit Modal (overlay) -->
40
+ <div class="hidden fixed inset-0 z-50 flex items-center justify-center p-4"
41
+ data-ruby-cms--visual-editor-target="modal">
42
+ <div class="absolute inset-0 bg-black/50" data-action="click->ruby-cms--visual-editor#closeModal"></div>
43
+ <div class="relative w-full max-w-xl bg-white rounded-lg shadow-xl flex flex-col max-h-[90vh]">
44
+ <!-- Modal header -->
45
+ <div class="flex items-start justify-between gap-4 px-6 py-4 border-b border-gray-200">
46
+ <div>
47
+ <h2 class="text-base font-semibold text-gray-900">Edit Content Block</h2>
48
+ <p class="text-sm text-gray-500 mt-0.5">
49
+ Block: <span class="inline-flex items-center rounded-md bg-blue-50 px-2 py-0.5 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-200" data-ruby-cms--visual-editor-target="blockKey"></span>
50
+ </p>
51
+ </div>
52
+ <button
53
+ type="button"
54
+ class="inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-900 transition-colors"
55
+ data-action="click->ruby-cms--visual-editor#closeModal">
56
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
57
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
58
+ </svg>
59
+ </button>
60
+ </div>
61
+
62
+ <!-- Modal body -->
63
+ <form class="flex flex-col flex-1 min-h-0 overflow-hidden"
64
+ data-action="submit->ruby-cms--visual-editor#saveContent keydown->ruby-cms--visual-editor#handleKeydown">
65
+ <div class="flex-1 overflow-y-auto px-6 py-4 space-y-4" data-ruby-cms--visual-editor-target="modalBody">
66
+ <div class="space-y-4">
67
+ <div>
68
+ <label class="block text-sm font-medium text-gray-700 mb-1" for="content-type">Content Type</label>
69
+ <select
70
+ id="content-type"
71
+ class="w-full 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"
72
+ data-ruby-cms--visual-editor-target="contentType"
73
+ data-action="change->ruby-cms--visual-editor#changeContentType">
74
+ <option value="text">Text</option>
75
+ <option value="rich_text">Rich Text</option>
76
+ <option value="image">Image</option>
77
+ <option value="link">Link</option>
78
+ <option value="list">List</option>
79
+ </select>
80
+ </div>
81
+
82
+ <div data-ruby-cms--visual-editor-target="textContainer">
83
+ <label class="block text-sm font-medium text-gray-700 mb-1" for="content">Content</label>
84
+ <textarea
85
+ id="content"
86
+ rows="10"
87
+ class="w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-teal-200 resize-y"
88
+ data-ruby-cms--visual-editor-target="contentInput"
89
+ data-action="input->ruby-cms--visual-editor#updateCharCount"></textarea>
90
+ </div>
91
+
92
+ <% if ::ContentBlock.respond_to?(:action_text_available?) && ::ContentBlock.action_text_available? %>
93
+ <div data-ruby-cms--visual-editor-target="richTextContainer">
94
+ <label class="block text-sm font-medium text-gray-700 mb-1" for="rich-content-input">Rich Content</label>
95
+ <input id="rich-content-input" type="hidden" name="rich_content" data-ruby-cms--visual-editor-target="richContentInput">
96
+ <trix-editor
97
+ input="rich-content-input"
98
+ class="w-full rounded-md border border-gray-200 bg-white text-sm shadow-sm focus:outline-none"
99
+ data-action="trix-change->ruby-cms--visual-editor#updateCharCount"></trix-editor>
100
+ </div>
101
+ <% else %>
102
+ <div data-ruby-cms--visual-editor-target="richTextContainer" style="display: none;">
103
+ <p class="text-sm text-gray-500">Rich text requires Action Text. Run: <code>bin/rails action_text:install</code></p>
104
+ </div>
105
+ <% end %>
106
+
107
+ <div class="flex items-center justify-between text-xs text-gray-400 pt-1">
108
+ <span data-ruby-cms--visual-editor-target="charCount">0 characters</span>
109
+ <span>Last updated: <span data-ruby-cms--visual-editor-target="lastUpdated">Never</span></span>
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ <!-- Modal footer -->
115
+ <div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-gray-200 bg-gray-50">
116
+ <button
117
+ type="button"
118
+ class="inline-flex h-9 items-center justify-center rounded-md border border-gray-200 bg-white px-4 text-sm font-medium text-gray-900 shadow-sm hover:bg-gray-50 transition-colors"
119
+ data-action="click->ruby-cms--visual-editor#closeModal">
120
+ Cancel
121
+ </button>
122
+ <button
123
+ type="submit"
124
+ class="inline-flex h-9 items-center justify-center rounded-md bg-teal-600 px-4 text-sm font-medium text-white shadow-sm hover:bg-teal-700 transition-colors"
125
+ data-ruby-cms--visual-editor-target="saveButton">
126
+ Save Changes
127
+ </button>
128
+ </div>
129
+ </form>
130
+ </div>
131
+ </div>
132
+
133
+ <!-- Success Toast -->
134
+ <div class="hidden fixed bottom-6 right-6 z-50" data-ruby-cms--visual-editor-target="toast">
135
+ <div class="flex items-center gap-2 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800 shadow-lg">
136
+ <svg class="h-5 w-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
137
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
138
+ </svg>
139
+ <span data-ruby-cms--visual-editor-target="toastMessage">Content updated successfully</span>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </div>
@@ -0,0 +1,92 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Page Not Found (404)</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <style>
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
9
+ margin: 0;
10
+ padding: 0;
11
+ background: #f5f5f5;
12
+ color: #333;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ min-height: 100vh;
17
+ }
18
+ .error-container {
19
+ text-align: center;
20
+ padding: 40px 20px;
21
+ max-width: 600px;
22
+ }
23
+ h1 {
24
+ font-size: 72px;
25
+ font-weight: 700;
26
+ margin: 0 0 20px;
27
+ color: #dc2626;
28
+ }
29
+ h2 {
30
+ font-size: 24px;
31
+ font-weight: 600;
32
+ margin: 0 0 16px;
33
+ color: #374151;
34
+ }
35
+ p {
36
+ font-size: 16px;
37
+ line-height: 1.6;
38
+ color: #6b7280;
39
+ margin: 0 0 32px;
40
+ }
41
+ a {
42
+ display: inline-block;
43
+ padding: 12px 24px;
44
+ background: #2563eb;
45
+ color: white;
46
+ text-decoration: none;
47
+ border-radius: 6px;
48
+ font-weight: 500;
49
+ transition: background 0.2s;
50
+ }
51
+ a:hover {
52
+ background: #1d4ed8;
53
+ }
54
+ .path {
55
+ font-family: "Monaco", "Courier New", monospace;
56
+ font-size: 14px;
57
+ color: #991b1b;
58
+ background: #fee2e2;
59
+ padding: 8px 12px;
60
+ border-radius: 4px;
61
+ margin: 24px 0;
62
+ word-break: break-all;
63
+ }
64
+ </style>
65
+ </head>
66
+ <body>
67
+ <div class="error-container">
68
+ <h1>404</h1>
69
+ <h2>Page Not Found</h2>
70
+ <p>The page you are looking for doesn't exist or has been moved.</p>
71
+ <div class="path"><%= ERB::Util.html_escape(request.path.to_s) %></div>
72
+ <a href="/">Go to Homepage</a>
73
+ </div>
74
+ </body>
75
+ </html>
76
+ <div class="rounded-lg border border-gray-200/80 bg-white shadow-sm p-8 max-w-lg mx-auto mt-12">
77
+ <nav class="text-sm text-gray-500 mb-4">
78
+ <%= link_to t("ruby_cms.admin.navigation.admin", default: "Admin"), ruby_cms_admin_root_path, class: "hover:text-gray-700 transition-colors" %> /
79
+ <span><%= t("ruby_cms.errors.not_found", default: "Page Not Found") %></span>
80
+ </nav>
81
+
82
+ <h1 class="text-4xl font-bold text-gray-900 mb-2">404</h1>
83
+ <h2 class="text-lg font-semibold text-gray-700 mb-3"><%= t("ruby_cms.errors.page_not_found", default: "Page Not Found") %></h2>
84
+
85
+ <p class="text-sm text-gray-500 mb-6">
86
+ <%= t("ruby_cms.errors.not_found_message", default: "The page you are looking for doesn't exist or has been moved.") %>
87
+ </p>
88
+
89
+ <div class="flex items-center gap-2">
90
+ <%= link_to t("ruby_cms.errors.back_to_admin", default: "Back to Admin"), ruby_cms_admin_root_path, class: "inline-flex h-9 items-center justify-center rounded-md border border-gray-200 bg-white px-4 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 transition-colors no-underline" %>
91
+ </div>
92
+ </div>
@@ -0,0 +1,6 @@
1
+ # Test database configuration for gem specs
2
+ test:
3
+ adapter: sqlite3
4
+ database: ":memory:"
5
+ pool: 5
6
+ timeout: 5000
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Importmap pins for the RubyCms engine.
4
+ #
5
+ # This file is loaded by the host application via:
6
+ # app.config.importmap.paths << RubyCms::Engine.root.join("config/importmap.rb")
7
+ #
8
+ # Ensure the engine's app/javascript is in config.assets.paths (done by the engine).
9
+
10
+ # Pin the main RubyCms controllers index for easy registration
11
+ pin "ruby_cms", to: "controllers/ruby_cms/index.js", preload: true
12
+
13
+ # Some downstream UI libraries (e.g. RubyUI) pin `@floating-ui/dom` to Skypack.
14
+ # Skypack intermittently returns 500s, which can prevent Stimulus controllers from
15
+ # loading (tooltip/select/etc) and break the Visual Editor experience.
16
+ #
17
+ # We pin it here (in the engine) to a more reliable ESM endpoint. Importmap pins
18
+ # are evaluated in order; later pins override earlier ones.
19
+ pin "@floating-ui/dom", to: "https://cdn.jsdelivr.net/npm/@floating-ui/dom/+esm"
20
+
21
+ # Alias pins for ruby_cms/ namespace (used by index.js imports)
22
+ pin "ruby_cms/visual_editor_controller", to: "controllers/ruby_cms/visual_editor_controller.js"
23
+ pin "ruby_cms/mobile_menu_controller", to: "controllers/ruby_cms/mobile_menu_controller.js"
24
+ pin "ruby_cms/flash_messages_controller", to: "controllers/ruby_cms/flash_messages_controller.js"
25
+ pin "ruby_cms/bulk_action_table_controller",
26
+ to: "controllers/ruby_cms/bulk_action_table_controller.js"
27
+ pin "ruby_cms/toggle_controller", to: "controllers/ruby_cms/toggle_controller.js"
28
+ pin "ruby_cms/locale_tabs_controller", to: "controllers/ruby_cms/locale_tabs_controller.js"
29
+ pin "ruby_cms/visual_editor_header_controller",
30
+ to: "controllers/ruby_cms/visual_editor_header_controller.js"
31
+ pin "ruby_cms/clickable_row_controller", to: "controllers/ruby_cms/clickable_row_controller.js"
32
+ pin "ruby_cms/auto_save_preference_controller",
33
+ to: "controllers/ruby_cms/auto_save_preference_controller.js"
34
+ pin "ruby_cms/nav_order_sortable_controller",
35
+ to: "controllers/ruby_cms/nav_order_sortable_controller.js"
36
+ pin "ruby_cms/page_preview_controller", to: "controllers/ruby_cms/page_preview_controller.js"
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ en:
4
+ activemodel:
5
+ errors:
6
+ models:
7
+ ruby_cms/content_block:
8
+ attributes:
9
+ key:
10
+ reserved: "cannot start with a reserved prefix (e.g. admin_)"
11
+ image:
12
+ content_type_invalid: "must be PNG, JPEG, GIF, or WebP"
13
+ file_too_large: "must be under 5 MB"
14
+
15
+ ruby_cms:
16
+ nav:
17
+ settings: "Settings"
18
+ admin:
19
+ base:
20
+ authentication_required: "Authentication required."
21
+ not_authorized: "Not authorized."
22
+ locales:
23
+ en: "English"
24
+ nl: "Dutch"
25
+ de: "German"
26
+ fr: "French"
27
+ es: "Spanish"
28
+ settings:
29
+ title: "Settings"
30
+ subtitle: "Configure admin interface preferences"
31
+ pagination: "Pagination"
32
+ pagination_description: "Number of items to display per page in each table"
33
+ navigation: "Navigation"
34
+ navigation_description: "Control which pages appear in the navigation menu"
35
+ nav_order_label: "Menu order"
36
+ nav_order_hint: "Drag items to reorder the sidebar menu."
37
+ general: "General"
38
+ updated: "Setting '%{key}' updated successfully."
39
+ defaults_reset: "All settings have been reset to default values."
40
+ reset_defaults: "Reset to Defaults"
41
+ reset_confirm: "Reset all settings to default values?"
42
+ no_settings: "No settings in this category"
43
+ analytics:
44
+ title: "Analytics"
45
+ subtitle: "Traffic and engagement overview"
46
+ apply_filters: "Apply"
47
+ back_to_analytics: "Back to analytics"
48
+ page_details_title: "Page details"
49
+ visitor_details_title: "Visitor details"
50
+ total_page_views: "Total page views"
51
+ unique_visitors: "Unique visitors"
52
+ total_sessions: "Total sessions"
53
+ popular_pages: "Popular pages"
54
+ top_visitors: "Top visitors"
55
+ suspicious_activity: "Suspicious activity"
56
+ none_detected: "None detected in selected range."
57
+ no_data: "No data yet."
58
+ visitor_errors:
59
+ title: "Visitor errors"
60
+ item_name: "error"
61
+ time: "Time"
62
+ page: "Page"
63
+ error: "Error"
64
+ browser: "Browser"
65
+ ip: "IP"
66
+ status: "Status"
67
+ all: "All"
68
+ open: "Open"
69
+ resolved: "Resolved"
70
+ error_details: "Error details"
71
+ error_class: "Error class"
72
+ request_method: "Request method"
73
+ request_path: "Request path"
74
+ ip_address: "IP address"
75
+ occurred_at: "Occurred at"
76
+ error_message: "Error message"
77
+ codepath: "Code path"
78
+ stack_trace: "Stack trace"
79
+ request_parameters: "Request parameters"
80
+ referer: "Referer"
81
+ query_string: "Query string"
82
+ session_id: "Session ID"
83
+ user_agent: "User agent"
84
+ mark_as_resolved: "Mark as resolved"
85
+ mark_resolved_confirm: "Mark this error as resolved?"
86
+ back_to_errors: "Back to errors"
87
+ bulk_deleted: "%{count} error(s) deleted."
88
+ bulk_resolved: "%{count} error(s) marked as resolved."
89
+ content_blocks:
90
+ created: "Content block created."
91
+ updated: "Content block updated."
92
+ deleted: "Content block deleted."
93
+ locale_read_only: "(locale cannot be changed for existing blocks)"
94
+ select_locale: "Select locale"
95
+ edit_in_locale: "Edit in %{locale_name}"
96
+ other_languages: "Other languages"
97
+ content_type: "Content type"
98
+ title: "Title"
99
+ content: "Content"
100
+ rich_content: "Rich content"
101
+ published: "Published"
data/config/routes.rb ADDED
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyCms::Engine.routes.draw do # rubocop:disable Metrics/BlockLength
4
+ scope path: "admin", module: "ruby_cms/admin", as: "ruby_cms_admin" do # rubocop:disable Metrics/BlockLength
5
+ root to: "dashboard#index"
6
+
7
+ resources :content_blocks do
8
+ collection do
9
+ delete "bulk_delete", to: "content_blocks#bulk_delete"
10
+ patch "bulk_publish", to: "content_blocks#bulk_publish"
11
+ patch "bulk_unpublish", to: "content_blocks#bulk_unpublish"
12
+ end
13
+ end
14
+
15
+ resources :permissions, only: %i[index create destroy] do
16
+ collection do
17
+ delete "bulk_delete", to: "permissions#bulk_delete"
18
+ end
19
+ end
20
+
21
+ resources :visitor_errors, only: %i[index show] do
22
+ member do
23
+ patch :resolve
24
+ end
25
+ collection do
26
+ delete :bulk_delete, to: "visitor_errors#bulk_delete"
27
+ patch :bulk_mark_as_resolved, to: "visitor_errors#bulk_mark_as_resolved"
28
+ end
29
+ end
30
+
31
+ resources :analytics, only: %i[index] do
32
+ collection do
33
+ get :page_details
34
+ get :visitor_details
35
+ end
36
+ end
37
+
38
+ get "settings", to: "settings#index", as: :settings
39
+ patch "settings", to: "settings#update", as: nil
40
+ post "settings", to: "settings#update", as: nil
41
+ post "settings/nav_order", to: "settings#update_nav_order", as: :settings_nav_order
42
+ post "settings/reset_defaults", to: "settings#reset_defaults", as: :settings_reset_defaults
43
+
44
+ resources :users, only: %i[index create destroy] do
45
+ collection do
46
+ delete "bulk_delete", to: "users#bulk_delete"
47
+ end
48
+ resources :permissions, only: %i[index create destroy], controller: "user_permissions",
49
+ path: "permissions" do
50
+ collection do
51
+ delete "bulk_delete", to: "user_permissions#bulk_delete"
52
+ end
53
+ end
54
+ end
55
+
56
+ # Visual editor routes
57
+ get "visual_editor", to: "visual_editor#index", as: :visual_editor
58
+ get "visual_editor/page_preview", to: "visual_editor#page_preview",
59
+ as: :visual_editor_page_preview
60
+ patch "visual_editor/quick_update", to: "visual_editor#quick_update",
61
+ as: :visual_editor_quick_update
62
+
63
+ patch "locale", to: "locale#update", as: :locale
64
+ end
65
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRubyCmsPermissions < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :ruby_cms_permissions do |t|
6
+ t.string :key, null: false
7
+ t.string :name
8
+
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :ruby_cms_permissions, :key, unique: true
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRubyCmsUserPermissions < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :ruby_cms_user_permissions do |t|
6
+ t.references :user, null: false, foreign_key: false
7
+ t.references :permission, null: false, foreign_key: { to_table: :ruby_cms_permissions }
8
+
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :ruby_cms_user_permissions, %i[user_id permission_id], unique: true
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRubyCmsContentBlocks < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :ruby_cms_content_blocks do |t|
6
+ t.string :key, null: false
7
+ t.string :title
8
+ t.text :content
9
+ t.string :content_type, default: "text", null: false
10
+ t.boolean :published, default: false, null: false
11
+ t.references :updated_by, null: true, foreign_key: false
12
+
13
+ t.timestamps
14
+ end
15
+
16
+ add_index :ruby_cms_content_blocks, :key, unique: true
17
+ add_index :ruby_cms_content_blocks, %i[published content_type]
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddIndexesToRubyCmsTables < ActiveRecord::Migration[7.1]
4
+ def change
5
+ return if index_exists?(:ruby_cms_content_blocks, :updated_by_id)
6
+
7
+ add_index :ruby_cms_content_blocks, :updated_by_id
8
+ end
9
+ end