railspress-engine 0.1.2 → 1.2.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 (140) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +20 -0
  3. data/README.md +195 -25
  4. data/app/assets/javascripts/railspress/admin.js +39 -0
  5. data/app/assets/javascripts/railspress/markdown_mode.js +343 -0
  6. data/app/assets/stylesheets/application.css +0 -0
  7. data/app/assets/stylesheets/railspress/admin/badges.css +70 -0
  8. data/app/assets/stylesheets/railspress/admin/base.css +25 -0
  9. data/app/assets/stylesheets/railspress/admin/buttons.css +140 -0
  10. data/app/assets/stylesheets/railspress/admin/cards.css +52 -0
  11. data/app/assets/stylesheets/railspress/admin/components/exports.css +55 -0
  12. data/app/assets/stylesheets/railspress/admin/components/focal_point.css +801 -0
  13. data/app/assets/stylesheets/railspress/admin/components/imports.css +144 -0
  14. data/app/assets/stylesheets/railspress/admin/components/lexxy.css +156 -0
  15. data/app/assets/stylesheets/railspress/admin/filters.css +73 -0
  16. data/app/assets/stylesheets/railspress/admin/flash.css +26 -0
  17. data/app/assets/stylesheets/railspress/admin/forms.css +459 -0
  18. data/app/assets/stylesheets/railspress/admin/layout.css +256 -0
  19. data/app/assets/stylesheets/railspress/admin/lists.css +24 -0
  20. data/app/assets/stylesheets/railspress/admin/page.css +111 -0
  21. data/app/assets/stylesheets/railspress/admin/responsive.css +174 -0
  22. data/app/assets/stylesheets/railspress/admin/stats.css +43 -0
  23. data/app/assets/stylesheets/railspress/admin/tables.css +163 -0
  24. data/app/assets/stylesheets/railspress/admin/utilities.css +202 -0
  25. data/app/assets/stylesheets/railspress/admin/variables.css +58 -0
  26. data/app/assets/stylesheets/railspress/application.css +44 -13
  27. data/app/controllers/railspress/admin/base_controller.rb +6 -3
  28. data/app/controllers/railspress/admin/categories_controller.rb +1 -1
  29. data/app/controllers/railspress/admin/cms_transfers_controller.rb +49 -0
  30. data/app/controllers/railspress/admin/content_element_versions_controller.rb +12 -0
  31. data/app/controllers/railspress/admin/content_elements_controller.rb +143 -0
  32. data/app/controllers/railspress/admin/content_groups_controller.rb +69 -0
  33. data/app/controllers/railspress/admin/dashboard_controller.rb +6 -0
  34. data/app/controllers/railspress/admin/entities_controller.rb +157 -0
  35. data/app/controllers/railspress/admin/exports_controller.rb +55 -0
  36. data/app/controllers/railspress/admin/focal_points_controller.rb +100 -0
  37. data/app/controllers/railspress/admin/imports_controller.rb +63 -0
  38. data/app/controllers/railspress/admin/posts_controller.rb +58 -4
  39. data/app/controllers/railspress/admin/prototypes_controller.rb +30 -0
  40. data/app/controllers/railspress/admin/tags_controller.rb +1 -1
  41. data/app/controllers/railspress/application_controller.rb +1 -0
  42. data/app/helpers/railspress/admin_helper.rb +733 -0
  43. data/app/helpers/railspress/application_helper.rb +23 -0
  44. data/app/helpers/railspress/cms_helper.rb +319 -0
  45. data/app/javascript/railspress/controllers/cms_inline_editor_controller.js +147 -0
  46. data/app/javascript/railspress/controllers/content_element_form_controller.js +15 -0
  47. data/app/javascript/railspress/controllers/crop_controller.js +224 -0
  48. data/app/javascript/railspress/controllers/dropzone_controller.js +261 -0
  49. data/app/javascript/railspress/controllers/focal_point_controller.js +124 -0
  50. data/app/javascript/railspress/controllers/image_section_controller.js +94 -0
  51. data/app/javascript/railspress/controllers/index.js +37 -0
  52. data/app/javascript/railspress/index.js +62 -0
  53. data/app/jobs/railspress/export_posts_job.rb +16 -0
  54. data/app/jobs/railspress/import_posts_job.rb +44 -0
  55. data/app/models/concerns/railspress/has_focal_point.rb +242 -0
  56. data/app/models/concerns/railspress/soft_deletable.rb +23 -0
  57. data/app/models/concerns/railspress/taggable.rb +23 -0
  58. data/app/models/railspress/content_element.rb +103 -0
  59. data/app/models/railspress/content_element_version.rb +32 -0
  60. data/app/models/railspress/content_group.rb +39 -0
  61. data/app/models/railspress/export.rb +67 -0
  62. data/app/models/railspress/focal_point.rb +70 -0
  63. data/app/models/railspress/import.rb +65 -0
  64. data/app/models/railspress/post.rb +102 -15
  65. data/app/models/railspress/post_export_processor.rb +162 -0
  66. data/app/models/railspress/post_import_processor.rb +382 -0
  67. data/app/models/railspress/tag.rb +10 -3
  68. data/app/models/railspress/tagging.rb +11 -0
  69. data/app/services/railspress/content_export_service.rb +122 -0
  70. data/app/services/railspress/content_import_service.rb +228 -0
  71. data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
  72. data/app/views/active_storage/blobs/_blob.html.erb +1 -1
  73. data/app/views/layouts/railspress/admin.html.erb +3 -1
  74. data/app/views/railspress/admin/categories/index.html.erb +11 -15
  75. data/app/views/railspress/admin/cms_transfers/show.html.erb +167 -0
  76. data/app/views/railspress/admin/content_element_versions/show.html.erb +42 -0
  77. data/app/views/railspress/admin/content_elements/_form.html.erb +71 -0
  78. data/app/views/railspress/admin/content_elements/_inline_form.html.erb +32 -0
  79. data/app/views/railspress/admin/content_elements/_inline_form_frame.html.erb +6 -0
  80. data/app/views/railspress/admin/content_elements/edit.html.erb +6 -0
  81. data/app/views/railspress/admin/content_elements/index.html.erb +74 -0
  82. data/app/views/railspress/admin/content_elements/new.html.erb +6 -0
  83. data/app/views/railspress/admin/content_elements/show.html.erb +124 -0
  84. data/app/views/railspress/admin/content_groups/_form.html.erb +9 -0
  85. data/app/views/railspress/admin/content_groups/edit.html.erb +6 -0
  86. data/app/views/railspress/admin/content_groups/index.html.erb +42 -0
  87. data/app/views/railspress/admin/content_groups/new.html.erb +6 -0
  88. data/app/views/railspress/admin/content_groups/show.html.erb +92 -0
  89. data/app/views/railspress/admin/dashboard/index.html.erb +36 -1
  90. data/app/views/railspress/admin/entities/_form.html.erb +53 -0
  91. data/app/views/railspress/admin/entities/edit.html.erb +4 -0
  92. data/app/views/railspress/admin/entities/index.html.erb +74 -0
  93. data/app/views/railspress/admin/entities/new.html.erb +4 -0
  94. data/app/views/railspress/admin/entities/show.html.erb +117 -0
  95. data/app/views/railspress/admin/exports/show.html.erb +62 -0
  96. data/app/views/railspress/admin/imports/_instructions.html.erb +56 -0
  97. data/app/views/railspress/admin/imports/show.html.erb +137 -0
  98. data/app/views/railspress/admin/posts/_form.html.erb +102 -28
  99. data/app/views/railspress/admin/posts/_post_row.html.erb +40 -0
  100. data/app/views/railspress/admin/posts/index.html.erb +47 -36
  101. data/app/views/railspress/admin/posts/show.html.erb +55 -19
  102. data/app/views/railspress/admin/prototypes/image_section.html.erb +42 -0
  103. data/app/views/railspress/admin/shared/_dropzone.html.erb +84 -0
  104. data/app/views/railspress/admin/shared/_focal_point_editor.html.erb +102 -0
  105. data/app/views/railspress/admin/shared/_image_section.html.erb +159 -0
  106. data/app/views/railspress/admin/shared/_image_section_compact.html.erb +90 -0
  107. data/app/views/railspress/admin/shared/_image_section_editor.html.erb +171 -0
  108. data/app/views/railspress/admin/shared/_image_section_v2.html.erb +205 -0
  109. data/app/views/railspress/admin/shared/_sidebar.html.erb +73 -5
  110. data/app/views/railspress/admin/tags/index.html.erb +12 -16
  111. data/config/brakeman.ignore +18 -0
  112. data/config/importmap.rb +23 -0
  113. data/config/routes.rb +62 -1
  114. data/db/migrate/20241218000004_create_railspress_post_tags.rb +1 -1
  115. data/db/migrate/20241218000005_create_railspress_imports.rb +21 -0
  116. data/db/migrate/20241218000006_create_railspress_exports.rb +20 -0
  117. data/db/migrate/20241218000007_create_railspress_taggings.rb +20 -0
  118. data/db/migrate/20241218000008_drop_railspress_post_tags.rb +14 -0
  119. data/db/migrate/20241218000010_add_reading_time_to_railspress_posts.rb +5 -0
  120. data/db/migrate/20250105000002_create_railspress_focal_points.rb +20 -0
  121. data/db/migrate/20260206000001_create_railspress_content_groups.rb +18 -0
  122. data/db/migrate/20260206000002_create_railspress_content_elements.rb +21 -0
  123. data/db/migrate/20260206000003_create_railspress_content_element_versions.rb +20 -0
  124. data/db/migrate/20260207000001_add_unique_index_to_content_elements.rb +11 -0
  125. data/db/migrate/20260211112812_add_image_hint_to_railspress_content_elements.rb +7 -0
  126. data/db/migrate/20260211154040_add_required_to_railspress_content_elements.rb +5 -0
  127. data/lib/generators/railspress/entity/entity_generator.rb +89 -0
  128. data/lib/generators/railspress/entity/templates/migration.rb.tt +13 -0
  129. data/lib/generators/railspress/entity/templates/model.rb.tt +21 -0
  130. data/lib/generators/railspress/install/install_generator.rb +51 -40
  131. data/lib/generators/railspress/install/templates/initializer.rb +29 -0
  132. data/lib/railspress/engine.rb +38 -0
  133. data/lib/railspress/entity.rb +239 -0
  134. data/lib/railspress/version.rb +1 -1
  135. data/lib/railspress.rb +198 -8
  136. data/lib/tasks/railspress_tasks.rake +49 -4
  137. metadata +215 -21
  138. data/MIT-LICENSE +0 -20
  139. data/app/assets/stylesheets/railspress/admin.css +0 -1207
  140. data/app/models/railspress/post_tag.rb +0 -8
@@ -0,0 +1,74 @@
1
+ <% content_for :title, "Content Elements" %>
2
+
3
+ <%= rp_page_header "Content Elements", "New Element" => new_admin_content_element_path %>
4
+
5
+ <% if @content_groups.any? %>
6
+ <div class="rp-filter-bar">
7
+ <%= form_with url: admin_content_elements_path, method: :get, class: "rp-filter-form" do |f| %>
8
+ <div class="rp-form-group rp-form-group--inline">
9
+ <%= f.label :content_group_id, "Filter by group:", class: "rp-label" %>
10
+ <%= f.select :content_group_id,
11
+ options_from_collection_for_select(@content_groups, :id, :name, params[:content_group_id]),
12
+ { include_blank: "All Groups" },
13
+ { class: "rp-select", onchange: "this.form.requestSubmit()" } %>
14
+ </div>
15
+ <% end %>
16
+ </div>
17
+ <% end %>
18
+
19
+ <div class="rp-card">
20
+ <% if @content_elements.any? %>
21
+ <div class="rp-table--responsive">
22
+ <table class="rp-table">
23
+ <thead>
24
+ <tr>
25
+ <th><%= rp_table_header("Name") %></th>
26
+ <th><%= rp_table_header("Group") %></th>
27
+ <th><%= rp_table_header("Type") %></th>
28
+ <th><%= rp_table_header("Content") %></th>
29
+ <th><%= rp_table_header("Updated") %></th>
30
+ <th class="rp-table-actions"><%= rp_table_header("Actions") %></th>
31
+ </tr>
32
+ </thead>
33
+ <tbody>
34
+ <% @content_elements.each do |element| %>
35
+ <tr>
36
+ <td class="rp-table-primary">
37
+ <%= link_to admin_content_element_path(element), class: "rp-link" do %>
38
+ <% if element.required? %><strong><%= element.name %></strong><% else %><%= element.name %><% end %>
39
+ <% end %>
40
+ </td>
41
+ <td>
42
+ <% if element.content_group %>
43
+ <%= link_to element.content_group.name, admin_content_group_path(element.content_group), class: "rp-link" %>
44
+ <% else %>
45
+ <span class="rp-table-secondary">—</span>
46
+ <% end %>
47
+ </td>
48
+ <td><%= rp_badge(element.content_type.titleize, status: element.text? ? :published : :info) %></td>
49
+ <td class="rp-table-secondary">
50
+ <% if element.image? && element.image.attached? %>
51
+ <%= image_tag main_app.rails_blob_path(element.image, only_path: true), style: "max-height: 1.5rem; width: auto; border-radius: 2px; vertical-align: middle;" %>
52
+ <% else %>
53
+ <%= rp_truncated_text(element.text_content, length: 40) %>
54
+ <% end %>
55
+ </td>
56
+ <td class="rp-table-secondary"><%= element.updated_at.strftime("%b %d, %Y") %></td>
57
+ <td class="rp-table-actions">
58
+ <%= rp_table_action_icons(
59
+ edit_path: edit_admin_content_element_path(element),
60
+ delete_path: admin_content_element_path(element),
61
+ confirm: "Delete this content element?",
62
+ delete_disabled: element.required?,
63
+ disabled_title: "Required Element"
64
+ ) %>
65
+ </td>
66
+ </tr>
67
+ <% end %>
68
+ </tbody>
69
+ </table>
70
+ </div>
71
+ <% else %>
72
+ <%= rp_empty_state "No content elements yet.", link_text: "Create your first content element", link_path: new_admin_content_element_path %>
73
+ <% end %>
74
+ </div>
@@ -0,0 +1,6 @@
1
+ <% content_for :title, "New Content Element" %>
2
+
3
+ <h1 class="rp-page-title rp-page-title--standalone">New Content Element</h1>
4
+ <div class="rp-card rp-card--padded">
5
+ <%= render "form" %>
6
+ </div>
@@ -0,0 +1,124 @@
1
+ <% content_for :title, @content_element.name %>
2
+
3
+ <div class="rp-page-header">
4
+ <div>
5
+ <h1 class="rp-page-title"><%= @content_element.name %></h1>
6
+ <p class="rp-byline">
7
+ in <%= link_to @content_element.content_group.name, admin_content_group_path(@content_element.content_group), class: "rp-link" %>
8
+ </p>
9
+ </div>
10
+ <div class="rp-page-actions">
11
+ <%= link_to "Edit", edit_admin_content_element_path(@content_element), class: "rp-btn rp-btn--primary" %>
12
+ <%= link_to "Back", admin_content_elements_path, class: "rp-btn rp-btn--secondary" %>
13
+ </div>
14
+ </div>
15
+
16
+ <div class="rp-post-meta-card">
17
+ <div class="rp-meta-item">
18
+ <span class="rp-meta-label">Type</span>
19
+ <%= rp_badge(@content_element.content_type.titleize, status: @content_element.text? ? :published : :info) %>
20
+ </div>
21
+ <div class="rp-meta-item">
22
+ <span class="rp-meta-label">Required</span>
23
+ <%= rp_boolean_badge(@content_element.required) %>
24
+ </div>
25
+ <% if @content_element.image? && @content_element.image_hint.present? %>
26
+ <div class="rp-meta-item">
27
+ <span class="rp-meta-label">Image Hint</span>
28
+ <span class="rp-meta-value"><%= @content_element.image_hint %></span>
29
+ </div>
30
+ <% end %>
31
+ <div class="rp-meta-item">
32
+ <span class="rp-meta-label">Group</span>
33
+ <span class="rp-meta-value"><%= @content_element.content_group.name %></span>
34
+ </div>
35
+ <% if @content_element.position.present? %>
36
+ <div class="rp-meta-item">
37
+ <span class="rp-meta-label">Position</span>
38
+ <span class="rp-meta-value"><%= @content_element.position %></span>
39
+ </div>
40
+ <% end %>
41
+ <div class="rp-meta-item">
42
+ <span class="rp-meta-label">Versions</span>
43
+ <span class="rp-meta-value"><%= @content_element.version_count %></span>
44
+ </div>
45
+ <div class="rp-meta-item">
46
+ <span class="rp-meta-label">Created</span>
47
+ <span class="rp-meta-value"><%= @content_element.created_at.strftime("%b %d, %Y at %l:%M %p") %></span>
48
+ </div>
49
+ <div class="rp-meta-item">
50
+ <span class="rp-meta-label">Updated</span>
51
+ <span class="rp-meta-value"><%= @content_element.updated_at.strftime("%b %d, %Y at %l:%M %p") %></span>
52
+ </div>
53
+ </div>
54
+
55
+ <% if @content_element.image? && @content_element.image.attached? %>
56
+ <%= render "railspress/admin/shared/image_section_compact",
57
+ record: @content_element, attachment_name: :image,
58
+ label: "Element Image",
59
+ editor_url: image_editor_admin_content_element_path(@content_element) %>
60
+ <% end %>
61
+
62
+ <%# Current Content Preview %>
63
+ <div class="rp-card rp-card--padded">
64
+ <h3 class="rp-sidebar-title">Current Content</h3>
65
+ <% if @content_element.text? && @content_element.text_content.present? %>
66
+ <div class="rp-content-preview"><%= @content_element.text_content %></div>
67
+ <% elsif @content_element.image? && @content_element.image.attached? %>
68
+ <div class="rp-content-preview">
69
+ <%= image_tag main_app.url_for(@content_element.image), style: "max-width: 100%; height: auto; border-radius: 4px;" %>
70
+ </div>
71
+ <% else %>
72
+ <p class="rp-empty-state">No content set.</p>
73
+ <% end %>
74
+ </div>
75
+
76
+ <%# Version History %>
77
+ <% if @versions.any? %>
78
+ <div class="rp-card" style="margin-top: 1.5rem;">
79
+ <div class="rp-card__header">
80
+ <h3 class="rp-sidebar-title" style="padding: 1rem 1.25rem; margin: 0;">Version History</h3>
81
+ </div>
82
+ <div class="rp-table--responsive">
83
+ <table class="rp-table">
84
+ <thead>
85
+ <tr>
86
+ <th><%= rp_table_header("Version") %></th>
87
+ <th><%= rp_table_header("Content") %></th>
88
+ <th><%= rp_table_header("Created") %></th>
89
+ <th class="rp-table-actions"><%= rp_table_header("Actions") %></th>
90
+ </tr>
91
+ </thead>
92
+ <tbody>
93
+ <% @versions.each do |version| %>
94
+ <tr>
95
+ <td class="rp-table-primary">#<%= version.version_number %></td>
96
+ <td class="rp-table-secondary"><%= rp_truncated_text(version.text_content, length: 60) %></td>
97
+ <td class="rp-table-secondary"><%= version.created_at.strftime("%b %d, %Y %l:%M %p") %></td>
98
+ <td class="rp-table-actions">
99
+ <%= link_to admin_content_element_version_path(version), class: "rp-icon-btn", title: "View version" do %>
100
+ <%= rp_icon(:search) %>
101
+ <% end %>
102
+ </td>
103
+ </tr>
104
+ <% end %>
105
+ </tbody>
106
+ </table>
107
+ </div>
108
+ </div>
109
+ <% end %>
110
+
111
+ <%# API Usage %>
112
+ <div class="rp-card rp-card--padded" style="margin-top: 1.5rem;">
113
+ <h3 class="rp-sidebar-title">API Usage</h3>
114
+ <pre class="rp-code-block"><code># Get the value
115
+ Railspress::CMS.find("<%= @content_element.content_group.name %>").load("<%= @content_element.name %>").value
116
+
117
+ # In views
118
+ &lt;%= cms_value("<%= @content_element.content_group.name %>", "<%= @content_element.name %>") %&gt;
119
+
120
+ # With custom rendering
121
+ &lt;%= cms_element(group: "<%= @content_element.content_group.name %>", name: "<%= @content_element.name %>") do |value| %&gt;
122
+ &lt;h1&gt;&lt;%= value %&gt;&lt;/h1&gt;
123
+ &lt;% end %&gt;</code></pre>
124
+ </div>
@@ -0,0 +1,9 @@
1
+ <%= form_with model: [:admin, @content_group], class: "rp-form rp-form--narrow" do |form| %>
2
+ <%= rp_form_errors(@content_group) %>
3
+
4
+ <%= rp_string_field(form, :name, primary: true, placeholder: "e.g., Homepage, Footer, About Page", required: true) %>
5
+
6
+ <%= rp_text_field(form, :description, rows: 3, placeholder: "Brief description of this content group (optional)") %>
7
+
8
+ <%= rp_form_actions(form, admin_content_groups_path) %>
9
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <% content_for :title, "Edit Content Group" %>
2
+
3
+ <h1 class="rp-page-title rp-page-title--standalone">Edit Content Group</h1>
4
+ <div class="rp-card rp-card--padded">
5
+ <%= render "form" %>
6
+ </div>
@@ -0,0 +1,42 @@
1
+ <% content_for :title, "Content Groups" %>
2
+
3
+ <%= rp_page_header "Content Groups", "New Group" => new_admin_content_group_path %>
4
+
5
+ <div class="rp-card">
6
+ <% if @content_groups.any? %>
7
+ <div class="rp-table--responsive">
8
+ <table class="rp-table">
9
+ <thead>
10
+ <tr>
11
+ <th><%= rp_table_header("Name") %></th>
12
+ <th><%= rp_table_header("Description") %></th>
13
+ <th><%= rp_table_header("Elements") %></th>
14
+ <th><%= rp_table_header("Created") %></th>
15
+ <th class="rp-table-actions"><%= rp_table_header("Actions") %></th>
16
+ </tr>
17
+ </thead>
18
+ <tbody>
19
+ <% @content_groups.each do |group| %>
20
+ <tr>
21
+ <td class="rp-table-primary">
22
+ <%= link_to group.name, admin_content_group_path(group), class: "rp-link" %>
23
+ </td>
24
+ <td class="rp-table-secondary"><%= rp_truncated_text(group.description, length: 60) %></td>
25
+ <td><%= rp_badge(group.element_count.to_s, status: group.element_count > 0 ? :published : :draft) %></td>
26
+ <td class="rp-table-secondary"><%= group.created_at.strftime("%b %d, %Y") %></td>
27
+ <td class="rp-table-actions">
28
+ <%= rp_table_action_icons(
29
+ edit_path: edit_admin_content_group_path(group),
30
+ delete_path: admin_content_group_path(group),
31
+ confirm: "Delete this content group and all its elements?"
32
+ ) %>
33
+ </td>
34
+ </tr>
35
+ <% end %>
36
+ </tbody>
37
+ </table>
38
+ </div>
39
+ <% else %>
40
+ <%= rp_empty_state "No content groups yet.", link_text: "Create your first content group", link_path: new_admin_content_group_path %>
41
+ <% end %>
42
+ </div>
@@ -0,0 +1,6 @@
1
+ <% content_for :title, "New Content Group" %>
2
+
3
+ <h1 class="rp-page-title rp-page-title--standalone">New Content Group</h1>
4
+ <div class="rp-card rp-card--padded">
5
+ <%= render "form" %>
6
+ </div>
@@ -0,0 +1,92 @@
1
+ <% content_for :title, @content_group.name %>
2
+
3
+ <div class="rp-page-header">
4
+ <div>
5
+ <h1 class="rp-page-title"><%= @content_group.name %></h1>
6
+ <% if @content_group.description.present? %>
7
+ <p class="rp-byline"><%= @content_group.description %></p>
8
+ <% end %>
9
+ </div>
10
+ <div class="rp-page-actions">
11
+ <%= link_to "New Element", new_admin_content_element_path(content_group_id: @content_group.id), class: "rp-btn rp-btn--primary" %>
12
+ <%= link_to "Edit", edit_admin_content_group_path(@content_group), class: "rp-btn rp-btn--secondary" %>
13
+ <%= link_to "Back", admin_content_groups_path, class: "rp-btn rp-btn--secondary" %>
14
+ </div>
15
+ </div>
16
+
17
+ <div class="rp-post-meta-card">
18
+ <div class="rp-meta-item">
19
+ <span class="rp-meta-label">Elements</span>
20
+ <span class="rp-meta-value"><%= @content_group.element_count %></span>
21
+ </div>
22
+ <div class="rp-meta-item">
23
+ <span class="rp-meta-label">Created</span>
24
+ <span class="rp-meta-value"><%= @content_group.created_at.strftime("%b %d, %Y") %></span>
25
+ </div>
26
+ <div class="rp-meta-item">
27
+ <span class="rp-meta-label">Updated</span>
28
+ <span class="rp-meta-value"><%= @content_group.updated_at.strftime("%b %d, %Y") %></span>
29
+ </div>
30
+ </div>
31
+
32
+ <div class="rp-card">
33
+ <% if @content_elements.any? %>
34
+ <div class="rp-table--responsive">
35
+ <table class="rp-table">
36
+ <thead>
37
+ <tr>
38
+ <th><%= rp_table_header("Name") %></th>
39
+ <th><%= rp_table_header("Type") %></th>
40
+ <th><%= rp_table_header("Content") %></th>
41
+ <th><%= rp_table_header("Position") %></th>
42
+ <th><%= rp_table_header("Updated") %></th>
43
+ <th class="rp-table-actions"><%= rp_table_header("Actions") %></th>
44
+ </tr>
45
+ </thead>
46
+ <tbody>
47
+ <% @content_elements.each do |element| %>
48
+ <tr>
49
+ <td class="rp-table-primary">
50
+ <%= link_to admin_content_element_path(element), class: "rp-link" do %>
51
+ <% if element.required? %><strong><%= element.name %></strong><% else %><%= element.name %><% end %>
52
+ <% end %>
53
+ </td>
54
+ <td><%= rp_badge(element.content_type.titleize, status: element.text? ? :published : :info) %></td>
55
+ <td class="rp-table-secondary">
56
+ <% if element.image? && element.image.attached? %>
57
+ <%= image_tag main_app.rails_blob_path(element.image, only_path: true), style: "max-height: 1.5rem; width: auto; border-radius: 2px; vertical-align: middle;" %>
58
+ <% else %>
59
+ <%= rp_truncated_text(element.text_content, length: 50) %>
60
+ <% end %>
61
+ </td>
62
+ <td><%= element.position || "—" %></td>
63
+ <td class="rp-table-secondary"><%= element.updated_at.strftime("%b %d, %Y") %></td>
64
+ <td class="rp-table-actions">
65
+ <%= rp_table_action_icons(
66
+ edit_path: edit_admin_content_element_path(element),
67
+ delete_path: admin_content_element_path(element),
68
+ confirm: "Delete this content element?",
69
+ delete_disabled: element.required?,
70
+ disabled_title: "Required Element"
71
+ ) %>
72
+ </td>
73
+ </tr>
74
+ <% end %>
75
+ </tbody>
76
+ </table>
77
+ </div>
78
+ <% else %>
79
+ <%= rp_empty_state "No content elements in this group yet.", link_text: "Create your first element", link_path: new_admin_content_element_path(content_group_id: @content_group.id) %>
80
+ <% end %>
81
+ </div>
82
+
83
+ <% if @content_group.element_count > 0 %>
84
+ <div class="rp-card rp-card--padded" style="margin-top: 1.5rem;">
85
+ <h3 class="rp-sidebar-title">API Usage</h3>
86
+ <pre class="rp-code-block"><code># Load a specific element value
87
+ Railspress::CMS.find("<%= @content_group.name %>").load("element_name").value
88
+
89
+ # In views
90
+ &lt;%= cms_value("<%= @content_group.name %>", "element_name") %&gt;</code></pre>
91
+ </div>
92
+ <% end %>
@@ -21,6 +21,16 @@
21
21
  <div class="rp-stat-value"><%= @tags_count %></div>
22
22
  <div class="rp-stat-label">Tags</div>
23
23
  </div>
24
+ <% if Railspress.cms_enabled? %>
25
+ <div class="rp-stat-card">
26
+ <div class="rp-stat-value"><%= @content_groups_count %></div>
27
+ <div class="rp-stat-label">Content Groups</div>
28
+ </div>
29
+ <div class="rp-stat-card">
30
+ <div class="rp-stat-value"><%= @content_elements_count %></div>
31
+ <div class="rp-stat-label">Content Elements</div>
32
+ </div>
33
+ <% end %>
24
34
  </div>
25
35
 
26
36
  <!-- Recent Posts -->
@@ -34,7 +44,7 @@
34
44
  <% @recent_posts.each do |post| %>
35
45
  <li class="rp-list-item">
36
46
  <%= link_to post.title, admin_post_path(post), class: "rp-link" %>
37
- <span class="rp-badge rp-badge--<%= post.status %>"><%= post.status.titleize %></span>
47
+ <span class="rp-badge rp-badge--<%= post.display_status %>"><%= post.display_status.titleize %></span>
38
48
  </li>
39
49
  <% end %>
40
50
  </ul>
@@ -43,3 +53,28 @@
43
53
  <% end %>
44
54
  </div>
45
55
  </div>
56
+
57
+ <% if Railspress.cms_enabled? %>
58
+ <!-- Recent Content Elements -->
59
+ <div class="rp-section">
60
+ <div class="rp-section-header">
61
+ <h2 class="rp-section-title">Recent Content</h2>
62
+ <%= link_to "View All", admin_content_elements_path, class: "rp-link" %>
63
+ </div>
64
+ <div class="rp-card">
65
+ <% if @recent_content_elements.any? %>
66
+ <ul class="rp-list">
67
+ <% @recent_content_elements.each do |element| %>
68
+ <li class="rp-list-item">
69
+ <%= link_to element.name, admin_content_element_path(element), class: "rp-link" %>
70
+ <span class="rp-table-secondary">in <%= element.content_group.name %></span>
71
+ <%= rp_badge(element.content_type.titleize, status: element.text? ? :published : :info) %>
72
+ </li>
73
+ <% end %>
74
+ </ul>
75
+ <% else %>
76
+ <p class="rp-empty-state">No content elements yet. <%= link_to "Create your first content group", new_admin_content_group_path, class: "rp-link" %>.</p>
77
+ <% end %>
78
+ </div>
79
+ </div>
80
+ <% end %>
@@ -0,0 +1,53 @@
1
+ <%= form_with model: @record,
2
+ url: @record.persisted? ? entity_show_path(@record) : entity_index_path,
3
+ class: "rp-form" do |form| %>
4
+
5
+ <%= rp_form_errors(@record) %>
6
+
7
+ <%
8
+ # Separate fields into main content vs sidebar metadata vs attachments
9
+ # Focal point images go in main content area (prominent placement)
10
+ main_types = [:string, :text, :rich_text, :lines, :focal_point_image]
11
+ sidebar_types = [:boolean, :integer, :decimal, :datetime, :date, :list]
12
+ attachment_types = [:attachment, :attachments]
13
+
14
+ main_fields = entity_config.fields.select { |_, f| main_types.include?(f[:type]) }
15
+ sidebar_fields = entity_config.fields.select { |_, f| sidebar_types.include?(f[:type]) }
16
+ attachment_fields = entity_config.fields.select { |_, f| attachment_types.include?(f[:type]) }
17
+ %>
18
+
19
+ <div class="rp-form-layout">
20
+ <!-- Main content column -->
21
+ <div class="rp-form-main">
22
+ <% main_fields.each_with_index do |(name, field), index| %>
23
+ <%= rp_render_field(form, name,
24
+ type: field[:type],
25
+ primary: index == 0 && field[:type] == :string,
26
+ required: @record.class.validators_on(name).any? { |v| v.is_a?(ActiveModel::Validations::PresenceValidator) }) %>
27
+ <% end %>
28
+ </div>
29
+
30
+ <% if sidebar_fields.any? || attachment_fields.any? %>
31
+ <!-- Sidebar column -->
32
+ <div class="rp-form-sidebar">
33
+ <% if sidebar_fields.any? %>
34
+ <%= rp_sidebar_section("Options") do %>
35
+ <% sidebar_fields.each do |name, field| %>
36
+ <%= rp_render_field(form, name, type: field[:type]) %>
37
+ <% end %>
38
+ <% end %>
39
+ <% end %>
40
+
41
+ <% attachment_fields.each do |name, field| %>
42
+ <%= rp_sidebar_section(name.to_s.humanize) do %>
43
+ <%= rp_render_field(form, name,
44
+ type: field[:type],
45
+ param_key: entity_config.param_key) %>
46
+ <% end %>
47
+ <% end %>
48
+ </div>
49
+ <% end %>
50
+ </div>
51
+
52
+ <%= rp_form_actions(form, entity_index_path) %>
53
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <% content_for :title, "Edit #{entity_config.singular_label}" %>
2
+
3
+ <h1 class="rp-page-title rp-page-title--standalone">Edit <%= entity_config.singular_label %></h1>
4
+ <%= render "form" %>
@@ -0,0 +1,74 @@
1
+ <% content_for :title, entity_config.label %>
2
+
3
+ <%= rp_page_header(entity_config.label, "New #{entity_config.singular_label}" => entity_new_path) %>
4
+
5
+ <%= rp_card do %>
6
+ <% if @records.any? %>
7
+ <% columns = entity_class.railspress_index_columns %>
8
+ <div class="rp-table--responsive">
9
+ <table class="rp-table">
10
+ <thead>
11
+ <tr>
12
+ <% columns.each do |col| %>
13
+ <th><%= col.to_s.humanize %></th>
14
+ <% end %>
15
+ <th class="rp-table-actions">Actions</th>
16
+ </tr>
17
+ </thead>
18
+ <tbody>
19
+ <% @records.each do |record| %>
20
+ <tr>
21
+ <% columns.each_with_index do |col, index| %>
22
+ <td<%= ' class="rp-table-truncate"'.html_safe if index == 0 %>>
23
+ <% value = record.public_send(col) %>
24
+ <% if index == 0 %>
25
+ <%= link_to entity_show_path(record), class: "rp-link rp-table-primary", title: value do %>
26
+ <%= rp_truncated_text(value, length: 60) %>
27
+ <% end %>
28
+ <% elsif value.is_a?(Time) || value.is_a?(DateTime) %>
29
+ <span class="rp-table-secondary" title="<%= value.strftime("%B %-d, %Y at %-I:%M %p") %>">
30
+ <span class="rp-table-date-full"><%= value.strftime("%-m/%-d/%Y") %></span>
31
+ <span class="rp-table-date-short"><%= value.strftime("%-m/%-d/%y") %></span>
32
+ </span>
33
+ <% elsif value.is_a?(Date) %>
34
+ <span class="rp-table-secondary" title="<%= value.strftime("%B %-d, %Y") %>">
35
+ <span class="rp-table-date-full"><%= value.strftime("%-m/%-d/%Y") %></span>
36
+ <span class="rp-table-date-short"><%= value.strftime("%-m/%-d/%y") %></span>
37
+ </span>
38
+ <% elsif value == true || value == false %>
39
+ <%= rp_boolean_badge(value) %>
40
+ <% elsif value.is_a?(Integer) %>
41
+ <%= value %>
42
+ <% else %>
43
+ <%= rp_truncated_text(value, length: 60) %>
44
+ <% end %>
45
+ </td>
46
+ <% end %>
47
+ <td class="rp-table-actions">
48
+ <%= link_to entity_edit_path(record), class: "rp-icon-btn", title: "Edit" do %>
49
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
50
+ <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
51
+ <path d="m15 5 4 4"/>
52
+ </svg>
53
+ <% end %>
54
+ <%= button_to entity_show_path(record), method: :delete,
55
+ data: { turbo_confirm: "Delete this #{entity_config.singular_label.downcase}?" },
56
+ class: "rp-icon-btn rp-icon-btn--danger", title: "Delete" do %>
57
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
58
+ <path d="M3 6h18"/>
59
+ <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
60
+ <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
61
+ </svg>
62
+ <% end %>
63
+ </td>
64
+ </tr>
65
+ <% end %>
66
+ </tbody>
67
+ </table>
68
+ </div>
69
+ <% else %>
70
+ <%= rp_empty_state("No #{entity_config.label.downcase} yet.",
71
+ link_text: "Create your first #{entity_config.singular_label.downcase}",
72
+ link_path: entity_new_path) %>
73
+ <% end %>
74
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <% content_for :title, "New #{entity_config.singular_label}" %>
2
+
3
+ <h1 class="rp-page-title rp-page-title--standalone">New <%= entity_config.singular_label %></h1>
4
+ <%= render "form" %>