helios-press 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +122 -0
- data/Rakefile +3 -0
- data/app/assets/stylesheets/helios/press/application.css +15 -0
- data/app/assets/stylesheets/helios/press/blocks.css +231 -0
- data/app/controllers/helios/press/admin/base_controller.rb +9 -0
- data/app/controllers/helios/press/admin/block_images_controller.rb +75 -0
- data/app/controllers/helios/press/admin/blocks_controller.rb +103 -0
- data/app/controllers/helios/press/admin/posts_controller.rb +72 -0
- data/app/controllers/helios/press/api/base_controller.rb +25 -0
- data/app/controllers/helios/press/api/posts_controller.rb +54 -0
- data/app/controllers/helios/press/application_controller.rb +6 -0
- data/app/controllers/helios/press/posts_controller.rb +9 -0
- data/app/helpers/helios/press/application_helper.rb +39 -0
- data/app/javascript/helios/press/controllers/blocks_controller.js +276 -0
- data/app/javascript/helios/press/controllers/image_block_controller.js +178 -0
- data/app/javascript/helios/press/controllers/text_block_controller.js +63 -0
- data/app/javascript/helios/press/index.js +6 -0
- data/app/jobs/helios/press/application_job.rb +6 -0
- data/app/mailers/helios/press/application_mailer.rb +8 -0
- data/app/models/helios/press/application_record.rb +7 -0
- data/app/models/helios/press/block.rb +34 -0
- data/app/models/helios/press/block_image.rb +16 -0
- data/app/models/helios/press/post.rb +39 -0
- data/app/views/helios/press/admin/blocks/_block.html.erb +25 -0
- data/app/views/helios/press/admin/blocks/_image_block.html.erb +47 -0
- data/app/views/helios/press/admin/blocks/_image_item.html.erb +38 -0
- data/app/views/helios/press/admin/blocks/_text_block.html.erb +39 -0
- data/app/views/helios/press/admin/blocks/_video_block.html.erb +43 -0
- data/app/views/helios/press/admin/posts/_blocks_container.html.erb +27 -0
- data/app/views/helios/press/admin/posts/_form.html.erb +48 -0
- data/app/views/helios/press/admin/posts/edit.html.erb +26 -0
- data/app/views/helios/press/admin/posts/index.html.erb +31 -0
- data/app/views/helios/press/admin/posts/new.html.erb +12 -0
- data/app/views/helios/press/posts/blocks/_image_container.html.erb +18 -0
- data/app/views/helios/press/posts/blocks/_text.html.erb +3 -0
- data/app/views/helios/press/posts/blocks/_video_container.html.erb +12 -0
- data/app/views/helios/press/posts/show.html.erb +45 -0
- data/app/views/layouts/helios/press/admin.html.erb +1 -0
- data/app/views/layouts/helios/press/application.html.erb +17 -0
- data/config/routes.rb +21 -0
- data/db/migrate/20250510000001_create_helios_press_posts.rb +18 -0
- data/db/migrate/20250510000002_create_helios_press_blocks.rb +14 -0
- data/db/migrate/20250510000003_create_helios_press_block_images.rb +13 -0
- data/lib/helios/press/configuration.rb +17 -0
- data/lib/helios/press/engine.rb +11 -0
- data/lib/helios/press/version.rb +5 -0
- data/lib/helios/press.rb +23 -0
- data/lib/tasks/helios/press_tasks.rake +4 -0
- metadata +148 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Helios
|
|
2
|
+
module Press
|
|
3
|
+
class BlockImage < ActiveRecord::Base
|
|
4
|
+
self.table_name = "helios_press_block_images"
|
|
5
|
+
|
|
6
|
+
belongs_to :block, class_name: "Helios::Press::Block", touch: true
|
|
7
|
+
|
|
8
|
+
has_one_attached :file
|
|
9
|
+
|
|
10
|
+
validates :position, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
|
11
|
+
validates :file, presence: true
|
|
12
|
+
|
|
13
|
+
acts_as_list scope: :block
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Helios
|
|
2
|
+
module Press
|
|
3
|
+
class Post < ActiveRecord::Base
|
|
4
|
+
self.table_name = "helios_press_posts"
|
|
5
|
+
|
|
6
|
+
has_many :blocks, -> { order(position: :asc) },
|
|
7
|
+
class_name: "Helios::Press::Block",
|
|
8
|
+
foreign_key: :post_id,
|
|
9
|
+
dependent: :destroy
|
|
10
|
+
|
|
11
|
+
accepts_nested_attributes_for :blocks, allow_destroy: true
|
|
12
|
+
|
|
13
|
+
validates :name, presence: true
|
|
14
|
+
validates :slug, presence: true, uniqueness: true
|
|
15
|
+
|
|
16
|
+
before_validation :generate_slug, if: -> { name.present? && (slug.blank? || name_changed?) }
|
|
17
|
+
|
|
18
|
+
scope :published, -> { where(published: true) }
|
|
19
|
+
scope :reverse_sorted, -> { order(updated_at: :desc) }
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def generate_slug
|
|
24
|
+
prefix = Helios::Press.configuration.post_slug_prefix
|
|
25
|
+
base_slug = name.parameterize
|
|
26
|
+
base_slug = "#{prefix}#{base_slug}" if prefix.present?
|
|
27
|
+
candidate_slug = base_slug
|
|
28
|
+
counter = 1
|
|
29
|
+
|
|
30
|
+
while self.class.where(slug: candidate_slug).where.not(id: id).exists?
|
|
31
|
+
candidate_slug = "#{base_slug}-#{counter}"
|
|
32
|
+
counter += 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
self.slug = candidate_slug
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<div id="block-<%= block.id %>"
|
|
2
|
+
class="content-block"
|
|
3
|
+
data-helios-press-blocks-target="block"
|
|
4
|
+
data-block-id="<%= block.id %>">
|
|
5
|
+
|
|
6
|
+
<!-- Drag handle in left margin -->
|
|
7
|
+
<div class="drag-handle">
|
|
8
|
+
<i class="bi bi-grip-vertical"></i>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div class="block-content">
|
|
12
|
+
<% case block.block_type %>
|
|
13
|
+
<% when "text" %>
|
|
14
|
+
<%= render "helios/press/admin/blocks/text_block", block: block, post: post %>
|
|
15
|
+
<% when "image_container" %>
|
|
16
|
+
<%= render "helios/press/admin/blocks/image_block", block: block, post: post %>
|
|
17
|
+
<% when "video_container" %>
|
|
18
|
+
<% if Helios::Press.videos_enabled? %>
|
|
19
|
+
<%= render "helios/press/admin/blocks/video_block", block: block, post: post %>
|
|
20
|
+
<% else %>
|
|
21
|
+
<div class="alert alert-warning mb-0">Video blocks require the helios-videos gem.</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
<% end %>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<div data-controller="helios-press-image-block"
|
|
2
|
+
data-helios-press-image-block-id-value="<%= block.id %>"
|
|
3
|
+
data-helios-press-image-block-post-id-value="<%= post.id %>"
|
|
4
|
+
data-helios-press-image-block-base-url-value="<%= helios_press.admin_post_block_images_path(post, block) %>"
|
|
5
|
+
data-helios-press-image-block-block-url-value="<%= helios_press.admin_post_block_path(post, block) %>"
|
|
6
|
+
class="image-block-container">
|
|
7
|
+
|
|
8
|
+
<!-- Block Controls -->
|
|
9
|
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
10
|
+
<div class="d-flex align-items-center gap-2">
|
|
11
|
+
<label class="form-label mb-0 small">Images per row:</label>
|
|
12
|
+
<select class="form-select form-select-sm"
|
|
13
|
+
style="width: auto;"
|
|
14
|
+
data-action="change->helios-press-image-block#updateColumns"
|
|
15
|
+
data-helios-press-image-block-target="columnsSelect">
|
|
16
|
+
<% (1..6).each do |n| %>
|
|
17
|
+
<option value="<%= n %>" <%= "selected" if (block.columns || 3) == n %>><%= n %></option>
|
|
18
|
+
<% end %>
|
|
19
|
+
</select>
|
|
20
|
+
</div>
|
|
21
|
+
<button type="button"
|
|
22
|
+
class="btn btn-sm btn-danger"
|
|
23
|
+
data-action="click->helios-press-blocks#deleteBlock">
|
|
24
|
+
Delete Block
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<!-- Images Grid -->
|
|
29
|
+
<div class="images-grid"
|
|
30
|
+
data-helios-press-image-block-target="grid"
|
|
31
|
+
data-columns="<%= block.columns || 3 %>"
|
|
32
|
+
style="--grid-columns: <%= block.columns || 3 %>;">
|
|
33
|
+
<% block.block_images.each do |block_image| %>
|
|
34
|
+
<%= render "helios/press/admin/blocks/image_item", block_image: block_image, block: block, post: post %>
|
|
35
|
+
<% end %>
|
|
36
|
+
|
|
37
|
+
<!-- Dropzone as a grid item -->
|
|
38
|
+
<div class="image-dropzone"
|
|
39
|
+
data-helios-press-image-block-target="dropzone"
|
|
40
|
+
data-action="dragover->helios-press-image-block#handleDragOver
|
|
41
|
+
dragleave->helios-press-image-block#handleDragLeave
|
|
42
|
+
drop->helios-press-image-block#handleDrop">
|
|
43
|
+
<i class="bi bi-plus-circle"></i>
|
|
44
|
+
<p>Drop to add</p>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<div class="image-item"
|
|
2
|
+
data-helios-press-image-block-target="image"
|
|
3
|
+
data-image-id="<%= block_image.id %>">
|
|
4
|
+
|
|
5
|
+
<!-- Image -->
|
|
6
|
+
<div class="image-wrapper">
|
|
7
|
+
<% if block_image.file.attached? %>
|
|
8
|
+
<%= image_tag block_image.file.variant(resize_to_limit: [800, 800]), class: "block-image" %>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<!-- Delete button -->
|
|
12
|
+
<button type="button"
|
|
13
|
+
class="btn btn-sm btn-danger image-delete"
|
|
14
|
+
data-action="click->helios-press-image-block#deleteImage">
|
|
15
|
+
<i class="bi bi-trash"></i>
|
|
16
|
+
</button>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<!-- Caption -->
|
|
20
|
+
<div class="image-caption">
|
|
21
|
+
<!-- View Mode -->
|
|
22
|
+
<div class="caption-view"
|
|
23
|
+
data-image-id="<%= block_image.id %>"
|
|
24
|
+
data-action="click->helios-press-image-block#editCaption">
|
|
25
|
+
<%= block_image.caption.presence || "Click to add caption..." %>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<!-- Edit Mode -->
|
|
29
|
+
<div class="caption-edit d-none">
|
|
30
|
+
<input type="text"
|
|
31
|
+
class="form-control form-control-sm"
|
|
32
|
+
value="<%= block_image.caption %>"
|
|
33
|
+
placeholder="Enter caption..."
|
|
34
|
+
data-image-id="<%= block_image.id %>"
|
|
35
|
+
data-action="blur->helios-press-image-block#saveCaption keydown.enter->helios-press-image-block#saveCaption keydown.esc->helios-press-image-block#cancelCaption">
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<div data-controller="helios-press-text-block"
|
|
2
|
+
data-helios-press-text-block-id-value="<%= block.id %>"
|
|
3
|
+
data-helios-press-text-block-post-id-value="<%= post.id %>"
|
|
4
|
+
data-helios-press-text-block-base-url-value="<%= helios_press.admin_post_block_path(post, block) %>"
|
|
5
|
+
data-helios-press-text-block-mode-value="view">
|
|
6
|
+
|
|
7
|
+
<!-- View Mode -->
|
|
8
|
+
<div data-helios-press-text-block-target="viewMode"
|
|
9
|
+
data-action="dblclick->helios-press-text-block#enterEditMode"
|
|
10
|
+
class="block-view-mode">
|
|
11
|
+
<% if block.content.present? %>
|
|
12
|
+
<%= helios_press_process_rich_text_links(block.content) %>
|
|
13
|
+
<% else %>
|
|
14
|
+
<p class="text-muted fst-italic">Double-click to add content...</p>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<!-- Edit Mode -->
|
|
19
|
+
<div data-helios-press-text-block-target="editMode" class="block-edit-mode d-none">
|
|
20
|
+
<%= form_with(model: block,
|
|
21
|
+
url: helios_press.admin_post_block_path(post, block),
|
|
22
|
+
data: { helios_press_text_block_target: "form" }) do |f| %>
|
|
23
|
+
<%= f.rich_text_area :content,
|
|
24
|
+
data: { helios_press_text_block_target: "editor" },
|
|
25
|
+
class: "form-control mb-2" %>
|
|
26
|
+
<div class="d-flex gap-2">
|
|
27
|
+
<%= f.submit "Save", class: "btn btn-primary btn-sm", data: { action: "click->helios-press-text-block#save" } %>
|
|
28
|
+
<button type="button"
|
|
29
|
+
class="btn btn-secondary btn-sm"
|
|
30
|
+
data-action="click->helios-press-text-block#cancelEdit">Cancel</button>
|
|
31
|
+
<button type="button"
|
|
32
|
+
class="btn btn-danger btn-sm"
|
|
33
|
+
data-action="click->helios-press-blocks#deleteBlock">
|
|
34
|
+
Delete
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
<% end %>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<% if block.respond_to?(:video) && block.video.present? %>
|
|
2
|
+
<% video = block.video %>
|
|
3
|
+
<div data-controller="helios-video-block"
|
|
4
|
+
data-helios-video-block-video-id-value="<%= video.id %>"
|
|
5
|
+
class="video-block-container">
|
|
6
|
+
<!-- Block Controls -->
|
|
7
|
+
<div class="d-flex justify-content-end mb-2">
|
|
8
|
+
<button type="button"
|
|
9
|
+
class="btn btn-sm btn-danger"
|
|
10
|
+
data-action="click->helios-press-blocks#deleteBlock">
|
|
11
|
+
Delete Block
|
|
12
|
+
</button>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<!-- Video Name (editable) -->
|
|
16
|
+
<div class="mb-3">
|
|
17
|
+
<!-- View Mode -->
|
|
18
|
+
<div class="name-view"
|
|
19
|
+
data-action="click->helios-video-block#editName"
|
|
20
|
+
style="cursor: pointer; padding: 0.5rem; border: 1px solid transparent; border-radius: 0.25rem;">
|
|
21
|
+
<%= video.name.presence || "Click to add name..." %>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<!-- Edit Mode -->
|
|
25
|
+
<div class="name-edit d-none">
|
|
26
|
+
<input type="text"
|
|
27
|
+
class="form-control form-control-sm"
|
|
28
|
+
value="<%= video.name %>"
|
|
29
|
+
placeholder="Enter video name..."
|
|
30
|
+
data-action="blur->helios-video-block#saveName keydown.enter->helios-video-block#saveName keydown.esc->helios-video-block#cancelName">
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- Video Player -->
|
|
35
|
+
<div class="video-player-wrapper">
|
|
36
|
+
<%= video.player_component.html_safe %>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<% else %>
|
|
40
|
+
<div class="alert alert-warning mb-0">
|
|
41
|
+
No video found for this block.
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<div id="blocks-container" data-helios-press-blocks-target="blocksContainer">
|
|
2
|
+
<!-- Placeholder at the top -->
|
|
3
|
+
<div class="block-placeholder"
|
|
4
|
+
data-helios-press-blocks-target="placeholder"
|
|
5
|
+
data-position="0"
|
|
6
|
+
data-action="click->helios-press-blocks#addBlock
|
|
7
|
+
dragover->helios-press-blocks#handlePlaceholderDragOver
|
|
8
|
+
dragleave->helios-press-blocks#handlePlaceholderDragLeave
|
|
9
|
+
drop->helios-press-blocks#handlePlaceholderDrop">
|
|
10
|
+
<div class="placeholder-line"></div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<% post.blocks.each do |block| %>
|
|
14
|
+
<%= render "helios/press/admin/blocks/block", block: block, post: post %>
|
|
15
|
+
|
|
16
|
+
<!-- Placeholder after each block -->
|
|
17
|
+
<div class="block-placeholder"
|
|
18
|
+
data-helios-press-blocks-target="placeholder"
|
|
19
|
+
data-position="<%= block.position + 1 %>"
|
|
20
|
+
data-action="click->helios-press-blocks#addBlock
|
|
21
|
+
dragover->helios-press-blocks#handlePlaceholderDragOver
|
|
22
|
+
dragleave->helios-press-blocks#handlePlaceholderDragLeave
|
|
23
|
+
drop->helios-press-blocks#handlePlaceholderDrop">
|
|
24
|
+
<div class="placeholder-line"></div>
|
|
25
|
+
</div>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<%= form_with(model: post, url: post.persisted? ? helios_press.admin_post_path(post) : helios_press.admin_posts_path, local: true) do |form| %>
|
|
2
|
+
<% if post.errors.any? %>
|
|
3
|
+
<div class="alert alert-danger">
|
|
4
|
+
<h4><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h4>
|
|
5
|
+
<ul class="mb-0">
|
|
6
|
+
<% post.errors.full_messages.each do |message| %>
|
|
7
|
+
<li><%= message %></li>
|
|
8
|
+
<% end %>
|
|
9
|
+
</ul>
|
|
10
|
+
</div>
|
|
11
|
+
<% end %>
|
|
12
|
+
|
|
13
|
+
<div class="mb-3">
|
|
14
|
+
<%= form.label :name, "Title", class: "form-label" %>
|
|
15
|
+
<%= form.text_field :name, class: "form-control", placeholder: "Enter post title" %>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="mb-3">
|
|
19
|
+
<%= form.label :slug, "Slug", class: "form-label" %>
|
|
20
|
+
<%= form.text_field :slug, class: "form-control", placeholder: "abc/xyz" %>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="mb-3">
|
|
24
|
+
<%= form.label :description, class: "form-label" %>
|
|
25
|
+
<%= form.text_area :description, class: "form-control", rows: 3, placeholder: "Brief description for social media previews" %>
|
|
26
|
+
<div class="form-text">This description will appear when the post is shared on social media</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="mb-3">
|
|
30
|
+
<%= form.label :keywords, class: "form-label" %>
|
|
31
|
+
<%= form.text_field :keywords, class: "form-control", placeholder: "Separate keywords with commas" %>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="mb-3">
|
|
35
|
+
<div class="form-check">
|
|
36
|
+
<%= form.check_box :published, class: "form-check-input" %>
|
|
37
|
+
<%= form.label :published, "Published", class: "form-check-label" %>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="d-flex gap-2">
|
|
42
|
+
<%= form.submit "Save", class: "btn btn-primary" %>
|
|
43
|
+
<% if post.persisted? && !post.published? %>
|
|
44
|
+
<%= form.submit "Publish Now", name: "publish", class: "btn btn-success", data: { turbo_confirm: "Publish this post now?" } %>
|
|
45
|
+
<% end %>
|
|
46
|
+
<%= link_to "Cancel", helios_press.admin_posts_path, class: "btn btn-secondary" %>
|
|
47
|
+
</div>
|
|
48
|
+
<% end %>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<div class="container py-5">
|
|
2
|
+
<div class="row">
|
|
3
|
+
<div class="col-lg-8">
|
|
4
|
+
<h1 class="mb-4">Edit Post</h1>
|
|
5
|
+
|
|
6
|
+
<!-- Basic Post Info -->
|
|
7
|
+
<div class="card mb-4">
|
|
8
|
+
<div class="card-body">
|
|
9
|
+
<%= render "form", post: @post %>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<!-- Blocks Editor -->
|
|
14
|
+
<div class="card">
|
|
15
|
+
<div class="card-header">
|
|
16
|
+
<h5 class="mb-0">Content Blocks</h5>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="card-body">
|
|
19
|
+
<div data-controller="helios-press-blocks" data-helios-press-blocks-post-id-value="<%= @post.id %>" data-helios-press-blocks-base-url-value="<%= helios_press.admin_post_blocks_path(@post) %>">
|
|
20
|
+
<%= render "blocks_container", post: @post %>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<div class="container py-5">
|
|
2
|
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
3
|
+
<h1>Posts</h1>
|
|
4
|
+
<%= link_to "New Post", helios_press.new_admin_post_path, class: "btn btn-primary" %>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<% if @posts.any? %>
|
|
8
|
+
<div class="list-group">
|
|
9
|
+
<% @posts.each do |post| %>
|
|
10
|
+
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
11
|
+
<div>
|
|
12
|
+
<h5 class="mb-1">
|
|
13
|
+
<%= link_to post.name, helios_press.edit_admin_post_path(post) %>
|
|
14
|
+
</h5>
|
|
15
|
+
<small class="text-muted">/<%= post.slug %></small>
|
|
16
|
+
<% if post.published? %>
|
|
17
|
+
<span class="badge bg-success ms-2">Published</span>
|
|
18
|
+
<% else %>
|
|
19
|
+
<span class="badge bg-secondary ms-2">Draft</span>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
<div>
|
|
23
|
+
<small class="text-muted"><%= post.updated_at.strftime("%m/%d/%y") %></small>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
28
|
+
<% else %>
|
|
29
|
+
<p class="text-muted">No posts yet. Create your first post!</p>
|
|
30
|
+
<% end %>
|
|
31
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<div class="image-block mb-5">
|
|
2
|
+
<div class="images-grid" style="--grid-columns: <%= block.columns || 3 %>;">
|
|
3
|
+
<% block.block_images.each do |block_image| %>
|
|
4
|
+
<div class="image-item">
|
|
5
|
+
<% if block_image.file.attached? %>
|
|
6
|
+
<%= image_tag block_image.file.variant(resize_to_limit: [1200, 1200]),
|
|
7
|
+
class: "img-fluid rounded",
|
|
8
|
+
alt: block_image.caption.presence || "" %>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% if block_image.caption.present? %>
|
|
11
|
+
<p class="image-caption text-muted mt-2 small">
|
|
12
|
+
<%= block_image.caption %>
|
|
13
|
+
</p>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<% if Helios::Press.videos_enabled? && block.respond_to?(:video) && block.video.present? %>
|
|
2
|
+
<% video = block.video %>
|
|
3
|
+
<div class="video-block mb-5">
|
|
4
|
+
<% if video.name.present? %>
|
|
5
|
+
<h3 class="h5 mb-3"><%= video.name %></h3>
|
|
6
|
+
<% end %>
|
|
7
|
+
|
|
8
|
+
<div class="video-player-wrapper">
|
|
9
|
+
<%= video.player_component.html_safe %>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
<% end %>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<% content_for :title, @post.name %>
|
|
2
|
+
|
|
3
|
+
<% content_for :head do %>
|
|
4
|
+
<%
|
|
5
|
+
first_image_block = @post.blocks.find { |b| b.block_type == "image_container" && b.block_images.any? }
|
|
6
|
+
%>
|
|
7
|
+
<% if @post.keywords.present? %>
|
|
8
|
+
<meta name="keywords" content="<%= @post.keywords %>">
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<meta property="og:title" content="<%= @post.name %>">
|
|
12
|
+
<meta property="og:type" content="article">
|
|
13
|
+
<meta property="og:url" content="<%= request.base_url %>/<%= @post.slug %>">
|
|
14
|
+
<% if @post.description.present? %>
|
|
15
|
+
<meta property="og:description" content="<%= @post.description %>">
|
|
16
|
+
<% end %>
|
|
17
|
+
<% if first_image_block&.block_images&.first&.file&.attached? %>
|
|
18
|
+
<meta property="og:image" content="<%= url_for(first_image_block.block_images.first.file.variant(resize_to_limit: [1200, 630])) %>">
|
|
19
|
+
<% end %>
|
|
20
|
+
<% if @post.published? %>
|
|
21
|
+
<meta property="article:published_time" content="<%= @post.created_at.iso8601 %>">
|
|
22
|
+
<meta property="article:modified_time" content="<%= @post.updated_at.iso8601 %>">
|
|
23
|
+
<% end %>
|
|
24
|
+
<% end %>
|
|
25
|
+
|
|
26
|
+
<div class="container my-5">
|
|
27
|
+
<article class="post">
|
|
28
|
+
<header class="post-header mb-5">
|
|
29
|
+
<h1 class="display-4 mb-3"><%= @post.name %></h1>
|
|
30
|
+
<p class="text-muted">
|
|
31
|
+
<time datetime="<%= @post.updated_at.iso8601 %>">
|
|
32
|
+
<%= @post.updated_at.strftime("%m/%d/%y") %>
|
|
33
|
+
</time>
|
|
34
|
+
</p>
|
|
35
|
+
</header>
|
|
36
|
+
|
|
37
|
+
<div class="post-content">
|
|
38
|
+
<% @post.blocks.each_with_index do |block, index| %>
|
|
39
|
+
<div id="block-<%= index %>">
|
|
40
|
+
<%= render "helios/press/posts/blocks/#{block.block_type}", block: block, block_index: index %>
|
|
41
|
+
</div>
|
|
42
|
+
<% end %>
|
|
43
|
+
</div>
|
|
44
|
+
</article>
|
|
45
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= render template: "layouts/application" %>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Helios press</title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
|
|
8
|
+
<%= yield :head %>
|
|
9
|
+
|
|
10
|
+
<%= stylesheet_link_tag "helios/press/application", media: "all" %>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
|
|
14
|
+
<%= yield %>
|
|
15
|
+
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Helios::Press::Engine.routes.draw do
|
|
2
|
+
namespace :admin do
|
|
3
|
+
root to: "posts#index"
|
|
4
|
+
resources :posts do
|
|
5
|
+
resources :blocks, only: [:create, :update, :destroy] do
|
|
6
|
+
collection do
|
|
7
|
+
patch :reorder
|
|
8
|
+
end
|
|
9
|
+
resources :images, controller: "block_images", only: [:create, :update, :destroy] do
|
|
10
|
+
collection do
|
|
11
|
+
patch :reorder
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
namespace :api do
|
|
19
|
+
resources :posts, only: [:create]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class CreateHeliosPressPosts < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :helios_press_posts do |t|
|
|
4
|
+
t.string :name, null: false
|
|
5
|
+
t.string :slug, null: false
|
|
6
|
+
t.text :description
|
|
7
|
+
t.string :keywords
|
|
8
|
+
t.boolean :published, default: false, null: false
|
|
9
|
+
t.string :external_id
|
|
10
|
+
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
add_index :helios_press_posts, :slug, unique: true
|
|
15
|
+
add_index :helios_press_posts, :external_id, unique: true
|
|
16
|
+
add_index :helios_press_posts, :published
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class CreateHeliosPressBlocks < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :helios_press_blocks do |t|
|
|
4
|
+
t.references :post, null: false, foreign_key: { to_table: :helios_press_posts }
|
|
5
|
+
t.string :block_type, null: false
|
|
6
|
+
t.integer :position, null: false
|
|
7
|
+
t.integer :columns, default: 3
|
|
8
|
+
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
add_index :helios_press_blocks, [:post_id, :position]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreateHeliosPressBlockImages < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :helios_press_block_images do |t|
|
|
4
|
+
t.references :block, null: false, foreign_key: { to_table: :helios_press_blocks }
|
|
5
|
+
t.integer :position, null: false
|
|
6
|
+
t.text :caption
|
|
7
|
+
|
|
8
|
+
t.timestamps
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
add_index :helios_press_block_images, [:block_id, :position]
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Helios
|
|
2
|
+
module Press
|
|
3
|
+
class Configuration
|
|
4
|
+
attr_accessor :admin_parent_controller,
|
|
5
|
+
:api_parent_controller,
|
|
6
|
+
:api_authentication,
|
|
7
|
+
:post_slug_prefix
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@admin_parent_controller = "ApplicationController"
|
|
11
|
+
@api_parent_controller = "ActionController::API"
|
|
12
|
+
@api_authentication = nil
|
|
13
|
+
@post_slug_prefix = nil
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Helios
|
|
2
|
+
module Press
|
|
3
|
+
class Engine < ::Rails::Engine
|
|
4
|
+
isolate_namespace Helios::Press
|
|
5
|
+
|
|
6
|
+
initializer "helios_press.assets" do |app|
|
|
7
|
+
app.config.assets.precompile += %w[helios/press/blocks.css] if app.config.respond_to?(:assets)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/helios/press.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require "helios/press/version"
|
|
2
|
+
require "helios/press/engine"
|
|
3
|
+
require "helios/press/configuration"
|
|
4
|
+
require "helios-videos"
|
|
5
|
+
require "helios-sitemap"
|
|
6
|
+
|
|
7
|
+
module Helios
|
|
8
|
+
module Press
|
|
9
|
+
class << self
|
|
10
|
+
def configuration
|
|
11
|
+
@configuration ||= Configuration.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def configure
|
|
15
|
+
yield(configuration)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def videos_enabled?
|
|
19
|
+
defined?(Helios::Videos)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|