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.
- checksums.yaml +4 -4
- data/LICENSE +20 -0
- data/README.md +195 -25
- data/app/assets/javascripts/railspress/admin.js +39 -0
- data/app/assets/javascripts/railspress/markdown_mode.js +343 -0
- data/app/assets/stylesheets/application.css +0 -0
- data/app/assets/stylesheets/railspress/admin/badges.css +70 -0
- data/app/assets/stylesheets/railspress/admin/base.css +25 -0
- data/app/assets/stylesheets/railspress/admin/buttons.css +140 -0
- data/app/assets/stylesheets/railspress/admin/cards.css +52 -0
- data/app/assets/stylesheets/railspress/admin/components/exports.css +55 -0
- data/app/assets/stylesheets/railspress/admin/components/focal_point.css +801 -0
- data/app/assets/stylesheets/railspress/admin/components/imports.css +144 -0
- data/app/assets/stylesheets/railspress/admin/components/lexxy.css +156 -0
- data/app/assets/stylesheets/railspress/admin/filters.css +73 -0
- data/app/assets/stylesheets/railspress/admin/flash.css +26 -0
- data/app/assets/stylesheets/railspress/admin/forms.css +459 -0
- data/app/assets/stylesheets/railspress/admin/layout.css +256 -0
- data/app/assets/stylesheets/railspress/admin/lists.css +24 -0
- data/app/assets/stylesheets/railspress/admin/page.css +111 -0
- data/app/assets/stylesheets/railspress/admin/responsive.css +174 -0
- data/app/assets/stylesheets/railspress/admin/stats.css +43 -0
- data/app/assets/stylesheets/railspress/admin/tables.css +163 -0
- data/app/assets/stylesheets/railspress/admin/utilities.css +202 -0
- data/app/assets/stylesheets/railspress/admin/variables.css +58 -0
- data/app/assets/stylesheets/railspress/application.css +44 -13
- data/app/controllers/railspress/admin/base_controller.rb +6 -3
- data/app/controllers/railspress/admin/categories_controller.rb +1 -1
- data/app/controllers/railspress/admin/cms_transfers_controller.rb +49 -0
- data/app/controllers/railspress/admin/content_element_versions_controller.rb +12 -0
- data/app/controllers/railspress/admin/content_elements_controller.rb +143 -0
- data/app/controllers/railspress/admin/content_groups_controller.rb +69 -0
- data/app/controllers/railspress/admin/dashboard_controller.rb +6 -0
- data/app/controllers/railspress/admin/entities_controller.rb +157 -0
- data/app/controllers/railspress/admin/exports_controller.rb +55 -0
- data/app/controllers/railspress/admin/focal_points_controller.rb +100 -0
- data/app/controllers/railspress/admin/imports_controller.rb +63 -0
- data/app/controllers/railspress/admin/posts_controller.rb +58 -4
- data/app/controllers/railspress/admin/prototypes_controller.rb +30 -0
- data/app/controllers/railspress/admin/tags_controller.rb +1 -1
- data/app/controllers/railspress/application_controller.rb +1 -0
- data/app/helpers/railspress/admin_helper.rb +733 -0
- data/app/helpers/railspress/application_helper.rb +23 -0
- data/app/helpers/railspress/cms_helper.rb +319 -0
- data/app/javascript/railspress/controllers/cms_inline_editor_controller.js +147 -0
- data/app/javascript/railspress/controllers/content_element_form_controller.js +15 -0
- data/app/javascript/railspress/controllers/crop_controller.js +224 -0
- data/app/javascript/railspress/controllers/dropzone_controller.js +261 -0
- data/app/javascript/railspress/controllers/focal_point_controller.js +124 -0
- data/app/javascript/railspress/controllers/image_section_controller.js +94 -0
- data/app/javascript/railspress/controllers/index.js +37 -0
- data/app/javascript/railspress/index.js +62 -0
- data/app/jobs/railspress/export_posts_job.rb +16 -0
- data/app/jobs/railspress/import_posts_job.rb +44 -0
- data/app/models/concerns/railspress/has_focal_point.rb +242 -0
- data/app/models/concerns/railspress/soft_deletable.rb +23 -0
- data/app/models/concerns/railspress/taggable.rb +23 -0
- data/app/models/railspress/content_element.rb +103 -0
- data/app/models/railspress/content_element_version.rb +32 -0
- data/app/models/railspress/content_group.rb +39 -0
- data/app/models/railspress/export.rb +67 -0
- data/app/models/railspress/focal_point.rb +70 -0
- data/app/models/railspress/import.rb +65 -0
- data/app/models/railspress/post.rb +102 -15
- data/app/models/railspress/post_export_processor.rb +162 -0
- data/app/models/railspress/post_import_processor.rb +382 -0
- data/app/models/railspress/tag.rb +10 -3
- data/app/models/railspress/tagging.rb +11 -0
- data/app/services/railspress/content_export_service.rb +122 -0
- data/app/services/railspress/content_import_service.rb +228 -0
- data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
- data/app/views/active_storage/blobs/_blob.html.erb +1 -1
- data/app/views/layouts/railspress/admin.html.erb +3 -1
- data/app/views/railspress/admin/categories/index.html.erb +11 -15
- data/app/views/railspress/admin/cms_transfers/show.html.erb +167 -0
- data/app/views/railspress/admin/content_element_versions/show.html.erb +42 -0
- data/app/views/railspress/admin/content_elements/_form.html.erb +71 -0
- data/app/views/railspress/admin/content_elements/_inline_form.html.erb +32 -0
- data/app/views/railspress/admin/content_elements/_inline_form_frame.html.erb +6 -0
- data/app/views/railspress/admin/content_elements/edit.html.erb +6 -0
- data/app/views/railspress/admin/content_elements/index.html.erb +74 -0
- data/app/views/railspress/admin/content_elements/new.html.erb +6 -0
- data/app/views/railspress/admin/content_elements/show.html.erb +124 -0
- data/app/views/railspress/admin/content_groups/_form.html.erb +9 -0
- data/app/views/railspress/admin/content_groups/edit.html.erb +6 -0
- data/app/views/railspress/admin/content_groups/index.html.erb +42 -0
- data/app/views/railspress/admin/content_groups/new.html.erb +6 -0
- data/app/views/railspress/admin/content_groups/show.html.erb +92 -0
- data/app/views/railspress/admin/dashboard/index.html.erb +36 -1
- data/app/views/railspress/admin/entities/_form.html.erb +53 -0
- data/app/views/railspress/admin/entities/edit.html.erb +4 -0
- data/app/views/railspress/admin/entities/index.html.erb +74 -0
- data/app/views/railspress/admin/entities/new.html.erb +4 -0
- data/app/views/railspress/admin/entities/show.html.erb +117 -0
- data/app/views/railspress/admin/exports/show.html.erb +62 -0
- data/app/views/railspress/admin/imports/_instructions.html.erb +56 -0
- data/app/views/railspress/admin/imports/show.html.erb +137 -0
- data/app/views/railspress/admin/posts/_form.html.erb +102 -28
- data/app/views/railspress/admin/posts/_post_row.html.erb +40 -0
- data/app/views/railspress/admin/posts/index.html.erb +47 -36
- data/app/views/railspress/admin/posts/show.html.erb +55 -19
- data/app/views/railspress/admin/prototypes/image_section.html.erb +42 -0
- data/app/views/railspress/admin/shared/_dropzone.html.erb +84 -0
- data/app/views/railspress/admin/shared/_focal_point_editor.html.erb +102 -0
- data/app/views/railspress/admin/shared/_image_section.html.erb +159 -0
- data/app/views/railspress/admin/shared/_image_section_compact.html.erb +90 -0
- data/app/views/railspress/admin/shared/_image_section_editor.html.erb +171 -0
- data/app/views/railspress/admin/shared/_image_section_v2.html.erb +205 -0
- data/app/views/railspress/admin/shared/_sidebar.html.erb +73 -5
- data/app/views/railspress/admin/tags/index.html.erb +12 -16
- data/config/brakeman.ignore +18 -0
- data/config/importmap.rb +23 -0
- data/config/routes.rb +62 -1
- data/db/migrate/20241218000004_create_railspress_post_tags.rb +1 -1
- data/db/migrate/20241218000005_create_railspress_imports.rb +21 -0
- data/db/migrate/20241218000006_create_railspress_exports.rb +20 -0
- data/db/migrate/20241218000007_create_railspress_taggings.rb +20 -0
- data/db/migrate/20241218000008_drop_railspress_post_tags.rb +14 -0
- data/db/migrate/20241218000010_add_reading_time_to_railspress_posts.rb +5 -0
- data/db/migrate/20250105000002_create_railspress_focal_points.rb +20 -0
- data/db/migrate/20260206000001_create_railspress_content_groups.rb +18 -0
- data/db/migrate/20260206000002_create_railspress_content_elements.rb +21 -0
- data/db/migrate/20260206000003_create_railspress_content_element_versions.rb +20 -0
- data/db/migrate/20260207000001_add_unique_index_to_content_elements.rb +11 -0
- data/db/migrate/20260211112812_add_image_hint_to_railspress_content_elements.rb +7 -0
- data/db/migrate/20260211154040_add_required_to_railspress_content_elements.rb +5 -0
- data/lib/generators/railspress/entity/entity_generator.rb +89 -0
- data/lib/generators/railspress/entity/templates/migration.rb.tt +13 -0
- data/lib/generators/railspress/entity/templates/model.rb.tt +21 -0
- data/lib/generators/railspress/install/install_generator.rb +51 -40
- data/lib/generators/railspress/install/templates/initializer.rb +29 -0
- data/lib/railspress/engine.rb +38 -0
- data/lib/railspress/entity.rb +239 -0
- data/lib/railspress/version.rb +1 -1
- data/lib/railspress.rb +198 -8
- data/lib/tasks/railspress_tasks.rake +49 -4
- metadata +215 -21
- data/MIT-LICENSE +0 -20
- data/app/assets/stylesheets/railspress/admin.css +0 -1207
- data/app/models/railspress/post_tag.rb +0 -8
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<% content_for :title, entity_config.singular_label %>
|
|
2
|
+
|
|
3
|
+
<%
|
|
4
|
+
# Separate focal point images from other fields for special display
|
|
5
|
+
focal_point_fields = entity_config.fields.select { |_, f| f[:type] == :focal_point_image }
|
|
6
|
+
other_fields = entity_config.fields.reject { |_, f| f[:type] == :focal_point_image }
|
|
7
|
+
%>
|
|
8
|
+
|
|
9
|
+
<div class="rp-page-header">
|
|
10
|
+
<h1 class="rp-page-title"><%= entity_config.singular_label %></h1>
|
|
11
|
+
<div class="rp-page-actions">
|
|
12
|
+
<%= link_to "Edit", entity_edit_path(@record), class: "rp-btn rp-btn--primary" %>
|
|
13
|
+
<%= link_to "Back to #{entity_config.label}", entity_index_path, class: "rp-btn rp-btn--secondary" %>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<%# Display focal point images prominently before the detail card %>
|
|
18
|
+
<% focal_point_fields.each do |name, field| %>
|
|
19
|
+
<% attachment = @record.public_send(name) %>
|
|
20
|
+
<% if attachment.attached? %>
|
|
21
|
+
<div class="rp-card rp-card--padded rp-image-section-card">
|
|
22
|
+
<%= render "railspress/admin/shared/image_section_compact",
|
|
23
|
+
record: @record,
|
|
24
|
+
attachment_name: name,
|
|
25
|
+
label: name.to_s.humanize %>
|
|
26
|
+
</div>
|
|
27
|
+
<% end %>
|
|
28
|
+
<% end %>
|
|
29
|
+
|
|
30
|
+
<div class="rp-card">
|
|
31
|
+
<dl class="rp-detail-list">
|
|
32
|
+
<% other_fields.each do |name, field| %>
|
|
33
|
+
<div class="rp-detail-item">
|
|
34
|
+
<dt class="rp-detail-label"><%= name.to_s.humanize %></dt>
|
|
35
|
+
<dd class="rp-detail-value">
|
|
36
|
+
<% value = @record.public_send(name) %>
|
|
37
|
+
<% case field[:type] %>
|
|
38
|
+
<% when :rich_text %>
|
|
39
|
+
<div class="rp-rich-text-content">
|
|
40
|
+
<%= value %>
|
|
41
|
+
</div>
|
|
42
|
+
<% when :boolean %>
|
|
43
|
+
<span class="rp-badge rp-badge--<%= value ? 'published' : 'draft' %>">
|
|
44
|
+
<%= value ? "Yes" : "No" %>
|
|
45
|
+
</span>
|
|
46
|
+
<% when :datetime %>
|
|
47
|
+
<%= value&.strftime("%B %d, %Y at %I:%M %p") || "—" %>
|
|
48
|
+
<% when :date %>
|
|
49
|
+
<%= value&.strftime("%B %d, %Y") || "—" %>
|
|
50
|
+
<% when :text %>
|
|
51
|
+
<div class="rp-text-content"><%= simple_format(value) %></div>
|
|
52
|
+
<% when :list %>
|
|
53
|
+
<% if value.is_a?(Array) && value.any? %>
|
|
54
|
+
<span class="rp-list-inline"><%= value.join(", ") %></span>
|
|
55
|
+
<% else %>
|
|
56
|
+
<span class="rp-text-muted">—</span>
|
|
57
|
+
<% end %>
|
|
58
|
+
<% when :lines %>
|
|
59
|
+
<% if value.is_a?(Array) && value.any? %>
|
|
60
|
+
<ul class="rp-list-block">
|
|
61
|
+
<% value.each do |item| %>
|
|
62
|
+
<li><%= item %></li>
|
|
63
|
+
<% end %>
|
|
64
|
+
</ul>
|
|
65
|
+
<% else %>
|
|
66
|
+
<span class="rp-text-muted">—</span>
|
|
67
|
+
<% end %>
|
|
68
|
+
<% when :attachments %>
|
|
69
|
+
<% if value.attached? %>
|
|
70
|
+
<div class="rp-gallery-grid">
|
|
71
|
+
<% value.each do |attachment| %>
|
|
72
|
+
<div class="rp-gallery-grid-item">
|
|
73
|
+
<% if attachment.image? %>
|
|
74
|
+
<%= link_to main_app.url_for(attachment), target: "_blank" do %>
|
|
75
|
+
<%= image_tag main_app.url_for(attachment), class: "rp-gallery-grid-image" %>
|
|
76
|
+
<% end %>
|
|
77
|
+
<% else %>
|
|
78
|
+
<%= link_to attachment.filename, main_app.url_for(attachment), class: "rp-link" %>
|
|
79
|
+
<% end %>
|
|
80
|
+
</div>
|
|
81
|
+
<% end %>
|
|
82
|
+
</div>
|
|
83
|
+
<% else %>
|
|
84
|
+
<span class="rp-text-muted">No images</span>
|
|
85
|
+
<% end %>
|
|
86
|
+
<% when :attachment %>
|
|
87
|
+
<% if value.attached? %>
|
|
88
|
+
<div class="rp-attachment-display">
|
|
89
|
+
<% if value.image? %>
|
|
90
|
+
<%= link_to main_app.url_for(value), target: "_blank" do %>
|
|
91
|
+
<%= image_tag main_app.url_for(value), class: "rp-attachment-image" %>
|
|
92
|
+
<% end %>
|
|
93
|
+
<% else %>
|
|
94
|
+
<%= link_to value.filename, main_app.url_for(value), class: "rp-link" %>
|
|
95
|
+
<% end %>
|
|
96
|
+
</div>
|
|
97
|
+
<% else %>
|
|
98
|
+
<span class="rp-text-muted">No file attached</span>
|
|
99
|
+
<% end %>
|
|
100
|
+
<% else %>
|
|
101
|
+
<%= value.presence || "—" %>
|
|
102
|
+
<% end %>
|
|
103
|
+
</dd>
|
|
104
|
+
</div>
|
|
105
|
+
<% end %>
|
|
106
|
+
|
|
107
|
+
<div class="rp-detail-item">
|
|
108
|
+
<dt class="rp-detail-label">Created</dt>
|
|
109
|
+
<dd class="rp-detail-value"><%= @record.created_at.strftime("%B %d, %Y at %I:%M %p") %></dd>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div class="rp-detail-item">
|
|
113
|
+
<dt class="rp-detail-label">Updated</dt>
|
|
114
|
+
<dd class="rp-detail-value"><%= @record.updated_at.strftime("%B %d, %Y at %I:%M %p") %></dd>
|
|
115
|
+
</div>
|
|
116
|
+
</dl>
|
|
117
|
+
</div>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<div class="rp-page-header">
|
|
2
|
+
<h1 class="rp-page-title">Export <%= @export_type.titleize %></h1>
|
|
3
|
+
<div class="rp-page-actions">
|
|
4
|
+
<%= link_to "Back to #{@export_type.titleize}", @back_path, class: "rp-btn rp-btn--secondary" %>
|
|
5
|
+
</div>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="rp-card rp-card--padded rp-export-format-card">
|
|
9
|
+
<div class="rp-export-format-content">
|
|
10
|
+
<div class="rp-export-format-info">
|
|
11
|
+
<strong>Export Format</strong>
|
|
12
|
+
<span>Each post exports as markdown with YAML frontmatter (title, slug, status, dates, category, tags, SEO fields, author). Header images included in <code>images/</code> folder.</span>
|
|
13
|
+
</div>
|
|
14
|
+
<%= form_with url: admin_exports_path, method: :post, class: "rp-export-format-action" do |form| %>
|
|
15
|
+
<%= hidden_field_tag "export[export_type]", @export_type %>
|
|
16
|
+
<%= form.submit "Export #{pluralize(@post_count, 'post')}", class: "rp-btn rp-btn--primary", disabled: @post_count == 0 %>
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<% if @recent_exports.any? %>
|
|
22
|
+
<div class="rp-card" style="margin-top: var(--rp-space-lg);">
|
|
23
|
+
<div class="rp-sidebar-section" style="background: transparent; border: none;">
|
|
24
|
+
<h3 class="rp-sidebar-title">Recent Exports</h3>
|
|
25
|
+
</div>
|
|
26
|
+
<table class="rp-table">
|
|
27
|
+
<thead>
|
|
28
|
+
<tr>
|
|
29
|
+
<th>File</th>
|
|
30
|
+
<th>Status</th>
|
|
31
|
+
<th>Results</th>
|
|
32
|
+
<th>Date</th>
|
|
33
|
+
<th></th>
|
|
34
|
+
</tr>
|
|
35
|
+
</thead>
|
|
36
|
+
<tbody>
|
|
37
|
+
<% @recent_exports.each do |export| %>
|
|
38
|
+
<tr>
|
|
39
|
+
<td class="rp-table-primary"><%= export.filename || "Processing..." %></td>
|
|
40
|
+
<td>
|
|
41
|
+
<span class="rp-badge rp-badge--<%= export.status %>">
|
|
42
|
+
<%= export.status.titleize %>
|
|
43
|
+
</span>
|
|
44
|
+
</td>
|
|
45
|
+
<td class="rp-table-secondary">
|
|
46
|
+
<%= export.success_count %>/<%= export.total_count %> exported
|
|
47
|
+
<% if export.error_count > 0 %>
|
|
48
|
+
<span class="rp-text-danger">(<%= export.error_count %> errors)</span>
|
|
49
|
+
<% end %>
|
|
50
|
+
</td>
|
|
51
|
+
<td class="rp-table-secondary"><%= export.created_at.strftime("%b %d, %Y %H:%M") %></td>
|
|
52
|
+
<td>
|
|
53
|
+
<% if export.completed? && export.file.attached? %>
|
|
54
|
+
<%= link_to "Download", download_admin_export_path(export), class: "rp-btn rp-btn--sm rp-btn--secondary" %>
|
|
55
|
+
<% end %>
|
|
56
|
+
</td>
|
|
57
|
+
</tr>
|
|
58
|
+
<% end %>
|
|
59
|
+
</tbody>
|
|
60
|
+
</table>
|
|
61
|
+
</div>
|
|
62
|
+
<% end %>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<div class="rp-sidebar-section">
|
|
2
|
+
<h3 class="rp-sidebar-title">Supported Files</h3>
|
|
3
|
+
<ul class="rp-import-list">
|
|
4
|
+
<li><strong>.md / .markdown</strong> - Markdown with optional frontmatter</li>
|
|
5
|
+
<li><strong>.txt</strong> - Plain text (title from filename)</li>
|
|
6
|
+
<li><strong>.zip</strong> - Archive containing multiple files</li>
|
|
7
|
+
</ul>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div class="rp-sidebar-section">
|
|
11
|
+
<h3 class="rp-sidebar-title">Frontmatter Fields</h3>
|
|
12
|
+
<ul class="rp-import-list">
|
|
13
|
+
<li><strong>title</strong> - Post title (required)</li>
|
|
14
|
+
<li><strong>slug</strong> - URL slug (auto-generated if blank)</li>
|
|
15
|
+
<li><strong>status</strong> - draft or published (default: draft)</li>
|
|
16
|
+
<li><strong>published_at</strong> - Publish date (default: today)</li>
|
|
17
|
+
<li><strong>author</strong> - Author name (case-insensitive match)</li>
|
|
18
|
+
<li><strong>category</strong> - Category name (case-insensitive match)</li>
|
|
19
|
+
<li><strong>tags</strong> - Comma-separated tag names</li>
|
|
20
|
+
<li><strong>header_image</strong> - Relative path in zip or URL</li>
|
|
21
|
+
<li><strong>meta_title</strong> - SEO title</li>
|
|
22
|
+
<li><strong>meta_description</strong> - SEO description</li>
|
|
23
|
+
</ul>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="rp-sidebar-section">
|
|
27
|
+
<h3 class="rp-sidebar-title">Example Markdown</h3>
|
|
28
|
+
<pre class="rp-code-block"><code>---
|
|
29
|
+
title: My Blog Post
|
|
30
|
+
status: published
|
|
31
|
+
published_at: 2024-01-15
|
|
32
|
+
author: John Doe
|
|
33
|
+
category: Technology
|
|
34
|
+
tags: ruby, rails, tutorial
|
|
35
|
+
header_image: images/hero.jpg
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
Your post content starts here.
|
|
39
|
+
|
|
40
|
+
Use **markdown** formatting for
|
|
41
|
+
rich text content.</code></pre>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="rp-sidebar-section">
|
|
45
|
+
<h3 class="rp-sidebar-title">Zip Structure</h3>
|
|
46
|
+
<pre class="rp-code-block"><code>my-posts.zip
|
|
47
|
+
├── post-one.md
|
|
48
|
+
├── post-two.md
|
|
49
|
+
├── images/
|
|
50
|
+
│ └── hero.jpg
|
|
51
|
+
└── drafts/
|
|
52
|
+
└── work-in-progress.md</code></pre>
|
|
53
|
+
<p class="rp-hint" style="margin-top: var(--rp-space-sm);">
|
|
54
|
+
Images referenced in frontmatter are attached from the zip.
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<div class="rp-page-header">
|
|
2
|
+
<h1 class="rp-page-title">Import <%= @import_type.titleize %></h1>
|
|
3
|
+
<div class="rp-page-actions">
|
|
4
|
+
<%= link_to "Back to #{@import_type.titleize}", @back_path, class: "rp-btn rp-btn--secondary" %>
|
|
5
|
+
</div>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="rp-import-layout">
|
|
9
|
+
<div class="rp-import-main">
|
|
10
|
+
<div class="rp-card rp-card--padded">
|
|
11
|
+
<%= form_with url: admin_imports_path, method: :post, multipart: true, class: "rp-form" do |form| %>
|
|
12
|
+
<%= hidden_field_tag "import[import_type]", @import_type %>
|
|
13
|
+
|
|
14
|
+
<label for="import_file" class="rp-dropzone" id="dropzone">
|
|
15
|
+
<div class="rp-dropzone-icon">
|
|
16
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
17
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
18
|
+
<polyline points="17 8 12 3 7 8"/>
|
|
19
|
+
<line x1="12" y1="3" x2="12" y2="15"/>
|
|
20
|
+
</svg>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="rp-dropzone-text">
|
|
23
|
+
<strong>Choose files or drag them here</strong>
|
|
24
|
+
<span>Supports .md, .markdown, .txt, or .zip</span>
|
|
25
|
+
</div>
|
|
26
|
+
<%= file_field_tag "import[file][]", id: "import_file", class: "rp-dropzone-input", multiple: true, accept: ".md,.markdown,.txt,.zip" %>
|
|
27
|
+
</label>
|
|
28
|
+
|
|
29
|
+
<div id="file-list" class="rp-file-list" hidden></div>
|
|
30
|
+
|
|
31
|
+
<div class="rp-form-actions">
|
|
32
|
+
<%= form.submit "Start Import", class: "rp-btn rp-btn--primary", id: "submit-btn", disabled: true %>
|
|
33
|
+
</div>
|
|
34
|
+
<% end %>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<% if @recent_imports.any? %>
|
|
38
|
+
<div class="rp-card" style="margin-top: var(--rp-space-lg);">
|
|
39
|
+
<div class="rp-sidebar-section" style="background: transparent; border: none;">
|
|
40
|
+
<h3 class="rp-sidebar-title">Recent Imports</h3>
|
|
41
|
+
</div>
|
|
42
|
+
<table class="rp-table">
|
|
43
|
+
<thead>
|
|
44
|
+
<tr>
|
|
45
|
+
<th>File</th>
|
|
46
|
+
<th>Status</th>
|
|
47
|
+
<th>Results</th>
|
|
48
|
+
<th>Date</th>
|
|
49
|
+
</tr>
|
|
50
|
+
</thead>
|
|
51
|
+
<tbody>
|
|
52
|
+
<% @recent_imports.each do |import| %>
|
|
53
|
+
<tr>
|
|
54
|
+
<td class="rp-table-primary"><%= import.filename || "Multiple files" %></td>
|
|
55
|
+
<td>
|
|
56
|
+
<span class="rp-badge rp-badge--<%= import.status %>">
|
|
57
|
+
<%= import.status.titleize %>
|
|
58
|
+
</span>
|
|
59
|
+
</td>
|
|
60
|
+
<td class="rp-table-secondary">
|
|
61
|
+
<%= import.success_count %>/<%= import.total_count %> succeeded
|
|
62
|
+
<% if import.error_count > 0 %>
|
|
63
|
+
<span class="rp-text-danger">(<%= import.error_count %> errors)</span>
|
|
64
|
+
<% end %>
|
|
65
|
+
</td>
|
|
66
|
+
<td class="rp-table-secondary"><%= import.created_at.strftime("%b %d, %Y %H:%M") %></td>
|
|
67
|
+
</tr>
|
|
68
|
+
<% end %>
|
|
69
|
+
</tbody>
|
|
70
|
+
</table>
|
|
71
|
+
</div>
|
|
72
|
+
<% end %>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="rp-import-sidebar">
|
|
76
|
+
<%= render "instructions" %>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<script>
|
|
81
|
+
(function() {
|
|
82
|
+
var dropzone = document.getElementById('dropzone');
|
|
83
|
+
var input = document.getElementById('import_file');
|
|
84
|
+
var fileList = document.getElementById('file-list');
|
|
85
|
+
var submitBtn = document.getElementById('submit-btn');
|
|
86
|
+
|
|
87
|
+
// Drag and drop
|
|
88
|
+
['dragenter', 'dragover'].forEach(function(event) {
|
|
89
|
+
dropzone.addEventListener(event, function(e) {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
dropzone.classList.add('rp-dropzone--active');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
['dragleave', 'drop'].forEach(function(event) {
|
|
96
|
+
dropzone.addEventListener(event, function(e) {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
dropzone.classList.remove('rp-dropzone--active');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
dropzone.addEventListener('drop', function(e) {
|
|
103
|
+
input.files = e.dataTransfer.files;
|
|
104
|
+
updateFileList();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// File input change
|
|
108
|
+
input.addEventListener('change', updateFileList);
|
|
109
|
+
|
|
110
|
+
function updateFileList() {
|
|
111
|
+
var files = input.files;
|
|
112
|
+
if (files.length === 0) {
|
|
113
|
+
fileList.hidden = true;
|
|
114
|
+
submitBtn.disabled = true;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
var html = '';
|
|
119
|
+
for (var i = 0; i < files.length; i++) {
|
|
120
|
+
html += '<div class="rp-file-item">' +
|
|
121
|
+
'<span class="rp-file-name">' + files[i].name + '</span>' +
|
|
122
|
+
'<span class="rp-file-size">' + formatSize(files[i].size) + '</span>' +
|
|
123
|
+
'</div>';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fileList.innerHTML = html;
|
|
127
|
+
fileList.hidden = false;
|
|
128
|
+
submitBtn.disabled = false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function formatSize(bytes) {
|
|
132
|
+
if (bytes < 1024) return bytes + ' B';
|
|
133
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
134
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
135
|
+
}
|
|
136
|
+
})();
|
|
137
|
+
</script>
|
|
@@ -30,10 +30,84 @@
|
|
|
30
30
|
<p class="rp-hint">Leave blank to auto-generate from title</p>
|
|
31
31
|
</div>
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
<% if post_images_enabled? %>
|
|
34
|
+
<% attachment = @post.header_image %>
|
|
35
|
+
<% has_image = attachment.attached? && attachment.blob&.persisted? %>
|
|
36
|
+
|
|
37
|
+
<% if @post.persisted? && has_image %>
|
|
38
|
+
<%# Existing post with image - use turbo frame for focal point editing %>
|
|
39
|
+
<%= render "railspress/admin/shared/image_section_compact",
|
|
40
|
+
record: @post,
|
|
41
|
+
attachment_name: :header_image,
|
|
42
|
+
label: "Main Image" %>
|
|
43
|
+
<% else %>
|
|
44
|
+
<%# New post or no image - show upload dropzone in form context %>
|
|
45
|
+
<div class="rp-form-group">
|
|
46
|
+
<label class="rp-label">Main Image</label>
|
|
47
|
+
<% if has_image %>
|
|
48
|
+
<%# Image uploaded but not saved yet - show preview %>
|
|
49
|
+
<div class="rp-image-section__compact">
|
|
50
|
+
<div class="rp-image-section__thumb">
|
|
51
|
+
<%= image_tag main_app.url_for(attachment.variant(resize_to_limit: [120, 80])), alt: "" %>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="rp-image-section__info">
|
|
54
|
+
<span class="rp-image-section__filename"><%= attachment.filename %></span>
|
|
55
|
+
<span class="rp-image-section__meta"><%= number_to_human_size(attachment.byte_size) %></span>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="rp-image-section__actions">
|
|
58
|
+
<label class="rp-btn rp-btn--outline rp-btn--sm">
|
|
59
|
+
Change
|
|
60
|
+
<%= form.file_field :header_image, accept: "image/*", class: "rp-sr-only", direct_upload: true %>
|
|
61
|
+
</label>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<p class="rp-hint">Save the post to enable focal point editing.</p>
|
|
65
|
+
<% else %>
|
|
66
|
+
<%= render "railspress/admin/shared/dropzone",
|
|
67
|
+
form: form,
|
|
68
|
+
field_name: :header_image,
|
|
69
|
+
prompt: "Click to upload main image" %>
|
|
70
|
+
<% end %>
|
|
71
|
+
</div>
|
|
72
|
+
<% end %>
|
|
73
|
+
<% end %>
|
|
74
|
+
|
|
75
|
+
<div class="rp-form-group" data-markdown-mode>
|
|
76
|
+
<div class="rp-editor-header">
|
|
77
|
+
<%= form.label :content, class: "rp-label" %>
|
|
78
|
+
<div class="rp-editor-mode-toggle">
|
|
79
|
+
<button type="button"
|
|
80
|
+
class="rp-btn rp-btn--small rp-btn--ghost"
|
|
81
|
+
data-markdown-toggle
|
|
82
|
+
aria-pressed="false"
|
|
83
|
+
title="Toggle markdown mode">
|
|
84
|
+
<span data-mode-label="rich">
|
|
85
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
86
|
+
<path d="M4 7V4h16v3M9 20h6M12 4v16"/>
|
|
87
|
+
</svg>
|
|
88
|
+
Markdown
|
|
89
|
+
</span>
|
|
90
|
+
<span data-mode-label="markdown" hidden>
|
|
91
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
92
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
|
93
|
+
<line x1="9" y1="9" x2="15" y2="9"/>
|
|
94
|
+
<line x1="9" y1="13" x2="15" y2="13"/>
|
|
95
|
+
<line x1="9" y1="17" x2="12" y2="17"/>
|
|
96
|
+
</svg>
|
|
97
|
+
Rich Text
|
|
98
|
+
</span>
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
35
102
|
<%= form.rich_text_area :content, class: "rp-rich-text" %>
|
|
36
|
-
<
|
|
103
|
+
<textarea data-markdown-textarea
|
|
104
|
+
class="rp-markdown-editor"
|
|
105
|
+
placeholder="Write in Markdown..."
|
|
106
|
+
hidden></textarea>
|
|
107
|
+
<p class="rp-hint">
|
|
108
|
+
<span data-mode-label="rich">Rich text editor powered by Lexxy. Click "Markdown" to edit raw markdown.</span>
|
|
109
|
+
<span data-mode-label="markdown" hidden>Editing in Markdown mode. Supports **bold**, *italic*, # headings, - lists, and more.</span>
|
|
110
|
+
</p>
|
|
37
111
|
</div>
|
|
38
112
|
</div>
|
|
39
113
|
|
|
@@ -50,6 +124,31 @@
|
|
|
50
124
|
class: "rp-select" %>
|
|
51
125
|
</div>
|
|
52
126
|
|
|
127
|
+
<div class="rp-form-group">
|
|
128
|
+
<%= form.label :published_at, "Publish Date", class: "rp-label" %>
|
|
129
|
+
<%= form.datetime_local_field :published_at,
|
|
130
|
+
class: "rp-input",
|
|
131
|
+
value: @post.published_at || Time.current %>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div class="rp-form-group">
|
|
135
|
+
<%= form.label :reading_time, "Reading Time (min)", class: "rp-label" %>
|
|
136
|
+
<div class="rp-input-with-button">
|
|
137
|
+
<%= form.number_field :reading_time,
|
|
138
|
+
class: "rp-input",
|
|
139
|
+
min: 1,
|
|
140
|
+
placeholder: @post.persisted? ? @post.calculate_reading_time : "Auto" %>
|
|
141
|
+
<% if @post.persisted? %>
|
|
142
|
+
<button type="button"
|
|
143
|
+
class="rp-btn rp-btn--secondary rp-btn--sm"
|
|
144
|
+
onclick="this.previousElementSibling.value = '<%= @post.calculate_reading_time %>'">
|
|
145
|
+
Calculate
|
|
146
|
+
</button>
|
|
147
|
+
<% end %>
|
|
148
|
+
</div>
|
|
149
|
+
<p class="rp-hint">Leave blank to auto-calculate from content</p>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
53
152
|
<div class="rp-form-group">
|
|
54
153
|
<%= form.label :category_id, "Category", class: "rp-label" %>
|
|
55
154
|
<%= form.collection_select :category_id, @categories, :id, :name,
|
|
@@ -76,31 +175,6 @@
|
|
|
76
175
|
</div>
|
|
77
176
|
</div>
|
|
78
177
|
|
|
79
|
-
<% if header_images_enabled? %>
|
|
80
|
-
<div class="rp-sidebar-section">
|
|
81
|
-
<h3 class="rp-sidebar-title">Header Image</h3>
|
|
82
|
-
|
|
83
|
-
<div class="rp-form-group">
|
|
84
|
-
<% if @post.header_image.attached? %>
|
|
85
|
-
<div class="rp-header-image-preview">
|
|
86
|
-
<%= image_tag main_app.url_for(@post.header_image.variant(resize_to_limit: [400, 300])), class: "rp-header-image-thumb" %>
|
|
87
|
-
</div>
|
|
88
|
-
<% end %>
|
|
89
|
-
|
|
90
|
-
<%= form.label :header_image, class: "rp-label" %>
|
|
91
|
-
<%= form.file_field :header_image, accept: "image/*", class: "rp-file-input" %>
|
|
92
|
-
<p class="rp-hint">Recommended: 1200x630px for social sharing</p>
|
|
93
|
-
|
|
94
|
-
<% if @post.header_image.attached? %>
|
|
95
|
-
<label class="rp-checkbox-label">
|
|
96
|
-
<%= form.check_box :remove_header_image, {}, "1", "0" %>
|
|
97
|
-
Remove current image
|
|
98
|
-
</label>
|
|
99
|
-
<% end %>
|
|
100
|
-
</div>
|
|
101
|
-
</div>
|
|
102
|
-
<% end %>
|
|
103
|
-
|
|
104
178
|
<div class="rp-sidebar-section">
|
|
105
179
|
<h3 class="rp-sidebar-title">SEO</h3>
|
|
106
180
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<tr>
|
|
2
|
+
<td class="rp-table-truncate">
|
|
3
|
+
<%= link_to post.title, admin_post_path(post), class: "rp-link rp-table-primary", title: post.title %>
|
|
4
|
+
<% if post.tags.any? %>
|
|
5
|
+
<div class="rp-tag-list">
|
|
6
|
+
<% post.tags.each do |tag| %>
|
|
7
|
+
<span class="rp-tag"><%= tag.name %></span>
|
|
8
|
+
<% end %>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
11
|
+
</td>
|
|
12
|
+
<td><%= post.category&.name || "—" %></td>
|
|
13
|
+
<% if authors_enabled? %>
|
|
14
|
+
<td><%= post.author&.public_send(Railspress.author_display_method) || "—" %></td>
|
|
15
|
+
<% end %>
|
|
16
|
+
<td>
|
|
17
|
+
<span class="rp-badge rp-badge--<%= post.display_status %>">
|
|
18
|
+
<%= post.display_status.titleize %>
|
|
19
|
+
</span>
|
|
20
|
+
</td>
|
|
21
|
+
<td class="rp-table-secondary" title="<%= post.published_at&.strftime("%B %-d, %Y at %-I:%M %p") %>">
|
|
22
|
+
<% if post.published_at %>
|
|
23
|
+
<span class="rp-table-date-full"><%= post.published_at.strftime("%-m/%-d/%Y %-I:%M%P") %></span>
|
|
24
|
+
<span class="rp-table-date-short"><%= post.published_at.strftime("%-m/%-d/%y") %></span>
|
|
25
|
+
<% else %>
|
|
26
|
+
—
|
|
27
|
+
<% end %>
|
|
28
|
+
</td>
|
|
29
|
+
<td class="rp-table-secondary" title="<%= post.created_at.strftime("%B %-d, %Y at %-I:%M %p") %>">
|
|
30
|
+
<span class="rp-table-date-full"><%= post.created_at.strftime("%-m/%-d/%Y") %></span>
|
|
31
|
+
<span class="rp-table-date-short"><%= post.created_at.strftime("%-m/%-d/%y") %></span>
|
|
32
|
+
</td>
|
|
33
|
+
<td class="rp-table-actions">
|
|
34
|
+
<%= rp_table_action_icons(
|
|
35
|
+
edit_path: edit_admin_post_path(post),
|
|
36
|
+
delete_path: admin_post_path(post),
|
|
37
|
+
confirm: "Delete this post?"
|
|
38
|
+
) %>
|
|
39
|
+
</td>
|
|
40
|
+
</tr>
|
|
@@ -1,55 +1,66 @@
|
|
|
1
1
|
<div class="rp-page-header">
|
|
2
2
|
<h1 class="rp-page-title">Posts</h1>
|
|
3
3
|
<div class="rp-page-actions">
|
|
4
|
+
<%= link_to "Export", typed_admin_exports_path(type: "posts"), class: "rp-btn rp-btn--secondary" %>
|
|
5
|
+
<%= link_to "Import", typed_admin_imports_path(type: "posts"), class: "rp-btn rp-btn--secondary" %>
|
|
4
6
|
<%= link_to "New Post", new_admin_post_path, class: "rp-btn rp-btn--primary" %>
|
|
5
7
|
</div>
|
|
6
8
|
</div>
|
|
7
9
|
|
|
10
|
+
<div class="rp-card">
|
|
11
|
+
<%= form_with url: admin_posts_path, method: :get, class: "rp-filter-bar", data: { turbo_frame: "_top" } do %>
|
|
12
|
+
<div class="rp-filter-bar__search">
|
|
13
|
+
<%= text_field_tag :q, params[:q], placeholder: "Search posts...", class: "rp-input" %>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="rp-filter-bar__filters">
|
|
16
|
+
<%= select_tag :category_id,
|
|
17
|
+
options_from_collection_for_select(@categories, :id, :name, params[:category_id]),
|
|
18
|
+
include_blank: "All Categories",
|
|
19
|
+
class: "rp-select" %>
|
|
20
|
+
<%= select_tag :status,
|
|
21
|
+
options_for_select([["All Statuses", ""], ["Draft", "draft"], ["Published", "published"], ["Scheduled", "scheduled"]], params[:status]),
|
|
22
|
+
class: "rp-select" %>
|
|
23
|
+
<%= submit_tag "Filter", class: "rp-btn rp-btn--secondary" %>
|
|
24
|
+
<% if params[:q].present? || params[:category_id].present? || params[:status].present? %>
|
|
25
|
+
<%= link_to "Clear", admin_posts_path, class: "rp-btn rp-btn--text" %>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
8
31
|
<div class="rp-card">
|
|
9
32
|
<div class="rp-table--responsive">
|
|
10
33
|
<table class="rp-table">
|
|
11
34
|
<thead>
|
|
12
35
|
<tr>
|
|
13
|
-
<th
|
|
14
|
-
<th
|
|
15
|
-
<% if authors_enabled? %><th
|
|
16
|
-
<th
|
|
17
|
-
<th
|
|
18
|
-
<th
|
|
36
|
+
<th><%= rp_sortable_header(:title, "Title", current_sort: @sort, current_direction: @direction) %></th>
|
|
37
|
+
<th><%= rp_sortable_header(:category, "Category", current_sort: @sort, current_direction: @direction) %></th>
|
|
38
|
+
<% if authors_enabled? %><th><%= rp_table_header("Author") %></th><% end %>
|
|
39
|
+
<th><%= rp_sortable_header(:status, "Status", current_sort: @sort, current_direction: @direction) %></th>
|
|
40
|
+
<th><%= rp_sortable_header(:published_at, "Published", current_sort: @sort, current_direction: @direction) %></th>
|
|
41
|
+
<th><%= rp_sortable_header(:created_at, "Created", current_sort: @sort, current_direction: @direction) %></th>
|
|
42
|
+
<th class="rp-table-actions"><%= rp_table_header("Actions") %></th>
|
|
19
43
|
</tr>
|
|
20
44
|
</thead>
|
|
21
45
|
<tbody>
|
|
22
|
-
|
|
23
|
-
<tr>
|
|
24
|
-
<td>
|
|
25
|
-
<%= link_to post.title, admin_post_path(post), class: "rp-link rp-table-primary" %>
|
|
26
|
-
<% if post.tags.any? %>
|
|
27
|
-
<div class="rp-tag-list">
|
|
28
|
-
<% post.tags.each do |tag| %>
|
|
29
|
-
<span class="rp-tag"><%= tag.name %></span>
|
|
30
|
-
<% end %>
|
|
31
|
-
</div>
|
|
32
|
-
<% end %>
|
|
33
|
-
</td>
|
|
34
|
-
<td><%= post.category&.name || "—" %></td>
|
|
35
|
-
<% if authors_enabled? %>
|
|
36
|
-
<td><%= post.author&.public_send(Railspress.author_display_method) || "—" %></td>
|
|
37
|
-
<% end %>
|
|
38
|
-
<td>
|
|
39
|
-
<span class="rp-badge rp-badge--<%= post.status %>">
|
|
40
|
-
<%= post.status.titleize %>
|
|
41
|
-
</span>
|
|
42
|
-
</td>
|
|
43
|
-
<td class="rp-table-secondary"><%= post.created_at.strftime("%b %d, %Y") %></td>
|
|
44
|
-
<td class="rp-table-actions">
|
|
45
|
-
<%= link_to "Edit", edit_admin_post_path(post), class: "rp-link" %>
|
|
46
|
-
<%= button_to "Delete", admin_post_path(post), method: :delete,
|
|
47
|
-
data: { turbo_confirm: "Delete this post?" },
|
|
48
|
-
class: "rp-link rp-link--danger" %>
|
|
49
|
-
</td>
|
|
50
|
-
</tr>
|
|
51
|
-
<% end %>
|
|
46
|
+
<%= render partial: "post_row", collection: @posts, as: :post %>
|
|
52
47
|
</tbody>
|
|
53
48
|
</table>
|
|
54
49
|
</div>
|
|
50
|
+
|
|
51
|
+
<% if @total_pages > 1 %>
|
|
52
|
+
<div class="rp-pagination">
|
|
53
|
+
<span class="rp-pagination__info">
|
|
54
|
+
Page <%= @page %> of <%= @total_pages %> (<%= @total_count %> posts)
|
|
55
|
+
</span>
|
|
56
|
+
<div class="rp-pagination__links">
|
|
57
|
+
<% if @page > 1 %>
|
|
58
|
+
<%= link_to "← Prev", url_for(request.query_parameters.merge(page: @page - 1)), class: "rp-btn rp-btn--secondary rp-btn--sm" %>
|
|
59
|
+
<% end %>
|
|
60
|
+
<% if @page < @total_pages %>
|
|
61
|
+
<%= link_to "Next →", url_for(request.query_parameters.merge(page: @page + 1)), class: "rp-btn rp-btn--secondary rp-btn--sm" %>
|
|
62
|
+
<% end %>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<% end %>
|
|
55
66
|
</div>
|