spina-blocks 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +154 -0
  3. data/Rakefile +4 -0
  4. data/app/controllers/spina/blocks/admin/blocks_controller.rb +105 -0
  5. data/app/controllers/spina/blocks/admin/categories_controller.rb +38 -0
  6. data/app/controllers/spina/blocks/admin/page_blocks_controller.rb +61 -0
  7. data/app/helpers/spina/blocks/blocks_helper.rb +57 -0
  8. data/app/models/spina/blocks/application_record.rb +9 -0
  9. data/app/models/spina/blocks/block.rb +26 -0
  10. data/app/models/spina/blocks/category.rb +18 -0
  11. data/app/models/spina/blocks/page_block.rb +14 -0
  12. data/app/models/spina/parts/block_collection.rb +18 -0
  13. data/app/models/spina/parts/block_reference.rb +15 -0
  14. data/app/overrides/spina/account_override.rb +19 -0
  15. data/app/overrides/spina/page_override.rb +6 -0
  16. data/app/views/spina/admin/hooks/blocks/_website_secondary_navigation.html.erb +5 -0
  17. data/app/views/spina/admin/parts/block_collections/_form.html.erb +24 -0
  18. data/app/views/spina/admin/parts/block_references/_form.html.erb +13 -0
  19. data/app/views/spina/blocks/admin/blocks/_button_block_content.html.erb +4 -0
  20. data/app/views/spina/blocks/admin/blocks/_button_block_settings.html.erb +4 -0
  21. data/app/views/spina/blocks/admin/blocks/_form.html.erb +53 -0
  22. data/app/views/spina/blocks/admin/blocks/_form_block_content.html.erb +8 -0
  23. data/app/views/spina/blocks/admin/blocks/_form_block_settings.html.erb +24 -0
  24. data/app/views/spina/blocks/admin/blocks/_new_block_form.html.erb +29 -0
  25. data/app/views/spina/blocks/admin/blocks/edit.html.erb +1 -0
  26. data/app/views/spina/blocks/admin/blocks/edit_content.html.erb +11 -0
  27. data/app/views/spina/blocks/admin/blocks/index.html.erb +81 -0
  28. data/app/views/spina/blocks/admin/blocks/new.html.erb +3 -0
  29. data/app/views/spina/blocks/admin/page_blocks/index.html.erb +90 -0
  30. data/config/locales/en.yml +37 -0
  31. data/config/routes.rb +27 -0
  32. data/db/migrate/1_create_spina_blocks_categories.rb +15 -0
  33. data/db/migrate/2_create_spina_blocks_blocks.rb +19 -0
  34. data/db/migrate/3_create_spina_blocks_page_blocks.rb +15 -0
  35. data/db/migrate/4_remove_name_from_spina_blocks_blocks.rb +8 -0
  36. data/lib/spina/blocks/engine.rb +70 -0
  37. data/lib/spina/blocks/version.rb +7 -0
  38. data/lib/spina-blocks.rb +5 -0
  39. metadata +93 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f004335deec2f8669d819540c01d8a58786f016b2a13de4d037da05f18301442
4
+ data.tar.gz: 623b17f09b53f84b957c9b112675951df21bc73d9de1a4719a3f8ed91bba7519
5
+ SHA512:
6
+ metadata.gz: e6d2c8a6d51c3c3393a2fb9511472272f0af777e150a6fab6c7fb6409a8a1450a87c6f01a98323272850a2896958a2ead13c20d1d9831cc4f6edfeb0daf901d4
7
+ data.tar.gz: c1a70f6285ba5888a6507fecdd0a432c8d23a74d174465cc6569a6a3a18a5f9b8efb44c1aaa375cea3cfb38fb95a068553be037676b68efa633e43850f0f2b40
data/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # Spina Blocks
2
+
3
+ A plugin for [Spina CMS](https://www.spinacms.com) that adds reusable block components. Blocks are independent content units with their own templates and fields that can be assembled into pages.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem "spina-blocks"
11
+ ```
12
+
13
+ Run:
14
+
15
+ ```bash
16
+ bundle install
17
+ rails db:migrate
18
+ ```
19
+
20
+ ## Configuration
21
+
22
+ ### Theme setup
23
+
24
+ In your theme initializer, add block templates and categories:
25
+
26
+ ```ruby
27
+ # config/initializers/themes/my_theme.rb
28
+ Spina::Theme.register do |theme|
29
+ theme.name = "my_theme"
30
+ theme.title = "My Theme"
31
+
32
+ # Enable the blocks plugin
33
+ theme.plugins = ["blocks"]
34
+
35
+ # Define all available parts (shared between pages and blocks)
36
+ theme.parts = [
37
+ { name: "headline", title: "Headline", part_type: "Spina::Parts::Line" },
38
+ { name: "body", title: "Body", part_type: "Spina::Parts::Text" },
39
+ { name: "image", title: "Image", part_type: "Spina::Parts::Image" },
40
+ { name: "button_text", title: "Button Text", part_type: "Spina::Parts::Line" },
41
+ { name: "button_url", title: "Button URL", part_type: "Spina::Parts::Line" }
42
+ ]
43
+
44
+ # Block categories (for organizing blocks in the library)
45
+ theme.block_categories = [
46
+ { name: "heroes", label: "Heroes" },
47
+ { name: "features", label: "Features" },
48
+ { name: "cta", label: "Call to Action" }
49
+ ]
50
+
51
+ # Block templates (which parts each block type uses)
52
+ theme.block_templates = [
53
+ {
54
+ name: "hero",
55
+ title: "Hero Section",
56
+ description: "Full-width hero with headline and image",
57
+ parts: ["headline", "body", "image", "button_text", "button_url"]
58
+ },
59
+ {
60
+ name: "cta_banner",
61
+ title: "CTA Banner",
62
+ description: "Call to action banner",
63
+ parts: ["headline", "body", "button_text", "button_url"]
64
+ }
65
+ ]
66
+
67
+ # Page templates (can use BlockCollection or BlockReference parts)
68
+ theme.view_templates = [
69
+ {
70
+ name: "homepage",
71
+ title: "Homepage",
72
+ parts: ["headline", "page_blocks"]
73
+ }
74
+ ]
75
+ end
76
+ ```
77
+
78
+ ### Block view templates
79
+
80
+ Create partials for each block template:
81
+
82
+ ```erb
83
+ <%# app/views/my_theme/blocks/_hero.html.erb %>
84
+ <section class="hero">
85
+ <h1><%= block_content(block, :headline) %></h1>
86
+ <div><%= block.content(:body) %></div>
87
+ </section>
88
+ ```
89
+
90
+ ### Using blocks on pages
91
+
92
+ #### Option 1: Page assembled from blocks (via PageBlocks)
93
+
94
+ In your page template, render all attached blocks:
95
+
96
+ ```erb
97
+ <%# app/views/my_theme/pages/homepage.html.erb %>
98
+ <%= render_blocks %>
99
+ ```
100
+
101
+ Manage which blocks appear on a page via Admin > Blocks > (select page).
102
+
103
+ #### Option 2: Block as a part type
104
+
105
+ Use `Spina::Parts::BlockReference` for a single block or `Spina::Parts::BlockCollection` for multiple blocks in your theme parts:
106
+
107
+ ```ruby
108
+ theme.parts = [
109
+ { name: "hero_block", title: "Hero Block", part_type: "Spina::Parts::BlockReference" },
110
+ { name: "page_blocks", title: "Page Blocks", part_type: "Spina::Parts::BlockCollection" }
111
+ ]
112
+ ```
113
+
114
+ Then in your template:
115
+
116
+ ```erb
117
+ <%# Single block reference %>
118
+ <%= render_block(content(:hero_block)) %>
119
+
120
+ <%# Block collection %>
121
+ <% content(:page_blocks)&.each do |block| %>
122
+ <%= render_block(block) %>
123
+ <% end %>
124
+ ```
125
+
126
+ ## Models
127
+
128
+ | Model | Description |
129
+ |-------|-------------|
130
+ | `Spina::Blocks::Block` | Reusable content block with template and parts |
131
+ | `Spina::Blocks::Category` | Block category for organizing the library |
132
+ | `Spina::Blocks::PageBlock` | Join model linking blocks to pages (with position) |
133
+
134
+ ## Admin interface
135
+
136
+ The plugin adds:
137
+
138
+ - **Blocks** link in the Content section of the admin sidebar
139
+ - Block library with category tabs for filtering
140
+ - Block editor with content fields (same as page editor)
141
+ - Page Blocks management page (per-page block assignment and ordering)
142
+
143
+ ## Helper methods
144
+
145
+ | Helper | Description |
146
+ |--------|-------------|
147
+ | `render_blocks(page)` | Render all blocks attached to a page via PageBlocks |
148
+ | `render_block(block)` | Render a single block using its template partial |
149
+ | `block_content(block, :part_name)` | Access a block's content |
150
+ | `block_has_content?(block, :part_name)` | Check if a block has content |
151
+
152
+ ## License
153
+
154
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'bundler/gem_tasks'
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Blocks
5
+ module Admin
6
+ class BlocksController < ::Spina::Admin::AdminController
7
+ admin_section :content
8
+
9
+ before_action :set_locale
10
+ before_action :set_block, only: %i[edit edit_content update destroy]
11
+ before_action :set_tabs, only: %i[edit update]
12
+
13
+ helper ::Spina::Admin::PagesHelper
14
+
15
+ def index
16
+ add_breadcrumb I18n.t('spina.blocks.title'), spina.blocks_admin_blocks_path
17
+
18
+ @block_templates = current_theme.try(:block_templates) || []
19
+
20
+ if params[:block_template].present?
21
+ @current_block_template = params[:block_template]
22
+ @blocks = Spina::Blocks::Block.where(block_template: @current_block_template).sorted
23
+ else
24
+ @current_block_template = nil
25
+ @blocks = Spina::Blocks::Block.sorted
26
+ end
27
+ end
28
+
29
+ def new
30
+ @block = Spina::Blocks::Block.new(block_template: params[:block_template])
31
+ end
32
+
33
+ def create
34
+ @block = Spina::Blocks::Block.new(block_params)
35
+ if @block.save
36
+ redirect_to spina.edit_blocks_admin_block_url(@block)
37
+ else
38
+ render turbo_stream: turbo_stream.update(
39
+ helpers.dom_id(@block, :new_block_form),
40
+ partial: 'new_block_form'
41
+ )
42
+ end
43
+ end
44
+
45
+ def edit
46
+ add_breadcrumb I18n.t('spina.blocks.title'), spina.blocks_admin_blocks_path, class: 'text-gray-400'
47
+ add_breadcrumb @block.title
48
+ end
49
+
50
+ def edit_content
51
+ @parts = current_theme.block_templates&.find do |bt|
52
+ bt[:name].to_s == @block.block_template.to_s
53
+ end&.dig(:parts) || []
54
+ end
55
+
56
+ def update
57
+ Mobility.locale = @locale
58
+ if @block.update(block_params)
59
+ flash[:success] = I18n.t('spina.blocks.saved')
60
+ redirect_to spina.edit_blocks_admin_block_url(@block, params: { locale: @locale })
61
+ else
62
+ add_breadcrumb I18n.t('spina.blocks.title'), spina.blocks_admin_blocks_path, class: 'text-gray-400'
63
+ Mobility.locale = I18n.locale
64
+ add_breadcrumb @block.title
65
+ flash.now[:error] = I18n.t('spina.blocks.couldnt_be_saved')
66
+ render :edit, status: :unprocessable_entity
67
+ end
68
+ end
69
+
70
+ def sort
71
+ params[:ids].each.with_index do |id, index|
72
+ Spina::Blocks::Block.where(id: id).update_all(position: index + 1)
73
+ end
74
+
75
+ flash.now[:info] = I18n.t('spina.blocks.sorting_saved')
76
+ render_flash
77
+ end
78
+
79
+ def destroy
80
+ flash[:info] = I18n.t('spina.blocks.deleted')
81
+ @block.destroy
82
+ redirect_to spina.blocks_admin_blocks_url
83
+ end
84
+
85
+ private
86
+
87
+ def set_locale
88
+ @locale = params[:locale] || I18n.default_locale
89
+ end
90
+
91
+ def set_block
92
+ @block = Spina::Blocks::Block.find(params[:id])
93
+ end
94
+
95
+ def set_tabs
96
+ @tabs = %w[block_content block_settings]
97
+ end
98
+
99
+ def block_params
100
+ params.require(:block).permit!
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Blocks
5
+ module Admin
6
+ class CategoriesController < ::Spina::Admin::AdminController
7
+ admin_section :content
8
+
9
+ def index
10
+ @categories = Spina::Blocks::Category.sorted
11
+ end
12
+
13
+ def edit
14
+ @category = Spina::Blocks::Category.find(params[:id])
15
+ add_breadcrumb I18n.t('spina.blocks.title'), spina.blocks_admin_blocks_path, class: 'text-gray-400'
16
+ add_breadcrumb @category.label
17
+ end
18
+
19
+ def update
20
+ @category = Spina::Blocks::Category.find(params[:id])
21
+ if @category.update(category_params)
22
+ flash[:success] = I18n.t('spina.block_categories.saved')
23
+ redirect_to spina.blocks_admin_blocks_url
24
+ else
25
+ flash.now[:error] = I18n.t('spina.block_categories.couldnt_be_saved')
26
+ render :edit, status: :unprocessable_entity
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def category_params
33
+ params.require(:category).permit(:name, :label, :position)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Blocks
5
+ module Admin
6
+ class PageBlocksController < ::Spina::Admin::AdminController
7
+ admin_section :content
8
+
9
+ before_action :set_page
10
+
11
+ def index
12
+ add_breadcrumb I18n.t('spina.website.pages'), spina.admin_pages_path, class: 'text-gray-400'
13
+ add_breadcrumb @page.title, spina.edit_admin_page_path(@page), class: 'text-gray-400'
14
+ add_breadcrumb I18n.t('spina.page_blocks.title')
15
+
16
+ @page_blocks = @page.page_blocks.sorted.includes(:block)
17
+ @available_blocks = Spina::Blocks::Block.active.sorted.where.not(id: @page.block_ids)
18
+ end
19
+
20
+ def create
21
+ @page_block = @page.page_blocks.build(page_block_params)
22
+ @page_block.position = @page.page_blocks.maximum(:position).to_i + 1
23
+
24
+ if @page_block.save
25
+ flash[:success] = I18n.t('spina.page_blocks.added')
26
+ else
27
+ flash[:error] = I18n.t('spina.page_blocks.couldnt_be_added')
28
+ end
29
+
30
+ redirect_to spina.blocks_admin_page_page_blocks_url(page_id: @page.id)
31
+ end
32
+
33
+ def destroy
34
+ @page_block = @page.page_blocks.find(params[:id])
35
+ @page_block.destroy
36
+ flash[:info] = I18n.t('spina.page_blocks.removed')
37
+ redirect_to spina.blocks_admin_page_page_blocks_url(page_id: @page.id)
38
+ end
39
+
40
+ def sort
41
+ params[:ids].each.with_index do |id, index|
42
+ @page.page_blocks.where(id: id).update_all(position: index + 1)
43
+ end
44
+
45
+ flash.now[:info] = I18n.t('spina.page_blocks.sorting_saved')
46
+ render_flash
47
+ end
48
+
49
+ private
50
+
51
+ def set_page
52
+ @page = ::Spina::Page.find(params[:page_id])
53
+ end
54
+
55
+ def page_block_params
56
+ params.require(:page_block).permit(:block_id)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Blocks
5
+ module BlocksHelper
6
+ # Render all blocks attached to the current page via PageBlocks
7
+ # Usage in a page template: <%= render_blocks %>
8
+ def render_blocks(page = nil)
9
+ page ||= current_page
10
+ return unless page.respond_to?(:page_blocks)
11
+
12
+ page.page_blocks.sorted.includes(:block).each do |page_block|
13
+ block = page_block.block
14
+ next unless block&.active?
15
+
16
+ concat render_block(block)
17
+ end
18
+ end
19
+
20
+ # Render a single block using its block_template partial
21
+ # Usage: <%= render_block(some_block) %>
22
+ def render_block(block)
23
+ return unless block&.active?
24
+
25
+ current_spina_theme = Spina::Current.theme || current_theme
26
+ theme_name = current_spina_theme.name.parameterize.underscore
27
+
28
+ partial_path = "#{theme_name}/blocks/#{block.block_template}"
29
+
30
+ if lookup_context.exists?(partial_path, [], true)
31
+ render partial: partial_path, locals: { block: block }
32
+ else
33
+ render_block_fallback(block)
34
+ end
35
+ end
36
+
37
+ # Access a block's content like page content
38
+ # Usage in block partial: block_content(block, :headline)
39
+ def block_content(block, part_name = nil)
40
+ block.content(part_name)
41
+ end
42
+
43
+ # Check if a block has content for a given part
44
+ def block_has_content?(block, part_name)
45
+ block.has_content?(part_name)
46
+ end
47
+
48
+ private
49
+
50
+ def render_block_fallback(block)
51
+ content_tag(:div, class: "spina-block spina-block--#{block.block_template}") do
52
+ content_tag(:p, block.title, class: 'spina-block__title')
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Blocks
5
+ class ApplicationRecord < ActiveRecord::Base
6
+ self.abstract_class = true
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Blocks
5
+ class Block < ApplicationRecord
6
+ include AttrJson::Record
7
+ include AttrJson::NestedAttributes
8
+ include Spina::Partable
9
+ include Spina::TranslatedContent
10
+
11
+ belongs_to :category, class_name: 'Spina::Blocks::Category', optional: true
12
+ has_many :page_blocks, class_name: 'Spina::Blocks::PageBlock', dependent: :destroy
13
+ has_many :pages, through: :page_blocks, class_name: 'Spina::Page'
14
+
15
+ validates :title, presence: true
16
+ validates :block_template, presence: true
17
+
18
+ scope :active, -> { where(active: true) }
19
+ scope :sorted, -> { order(:position) }
20
+
21
+ def to_s
22
+ title
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Blocks
5
+ class Category < ApplicationRecord
6
+ has_many :blocks, class_name: 'Spina::Blocks::Block', foreign_key: :category_id, dependent: :nullify
7
+
8
+ validates :name, presence: true, uniqueness: true
9
+ validates :label, presence: true
10
+
11
+ scope :sorted, -> { order(:position) }
12
+
13
+ def to_s
14
+ label
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Blocks
5
+ class PageBlock < ApplicationRecord
6
+ belongs_to :page, class_name: 'Spina::Page'
7
+ belongs_to :block, class_name: 'Spina::Blocks::Block'
8
+
9
+ validates :block_id, uniqueness: { scope: :page_id }
10
+
11
+ scope :sorted, -> { order(:position) }
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Parts
5
+ class BlockCollection < Base
6
+ attr_json :block_ids, :integer, array: true, default: -> { [] }
7
+
8
+ attr_accessor :options
9
+
10
+ def content
11
+ return [] if block_ids.blank?
12
+
13
+ blocks_by_id = ::Spina::Blocks::Block.active.where(id: block_ids).index_by(&:id)
14
+ block_ids.filter_map { |id| blocks_by_id[id] }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Parts
5
+ class BlockReference < Base
6
+ attr_json :block_id, :integer, default: nil
7
+
8
+ attr_accessor :options
9
+
10
+ def content
11
+ ::Spina::Blocks::Block.active.find_by(id: block_id)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ Spina::Account.class_eval do
4
+ after_save :bootstrap_block_categories
5
+
6
+ private
7
+
8
+ def bootstrap_block_categories
9
+ theme_config = Spina::Theme.find_by_name(theme)
10
+ return unless theme_config
11
+ return unless theme_config.respond_to?(:block_categories) && theme_config.block_categories.present?
12
+
13
+ theme_config.block_categories.each_with_index do |category, index|
14
+ Spina::Blocks::Category.where(name: category[:name])
15
+ .first_or_create(label: category[:label])
16
+ .update(label: category[:label], position: index)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ Spina::Page.class_eval do
4
+ has_many :page_blocks, class_name: 'Spina::Blocks::PageBlock', foreign_key: :page_id, dependent: :destroy
5
+ has_many :blocks, through: :page_blocks, class_name: 'Spina::Blocks::Block'
6
+ end
@@ -0,0 +1,5 @@
1
+ <%= render Spina::MainNavigation::LinkComponent.new(
2
+ t('spina.blocks.title'),
3
+ spina.blocks_admin_blocks_path,
4
+ active: controller_name.in?(%w(blocks categories page_blocks))
5
+ ) %>
@@ -0,0 +1,24 @@
1
+ <div class="mt-6">
2
+ <label class="block text-sm leading-5 font-medium text-gray-700"><%= f.object.title %></label>
3
+ <% if f.object.hint.present? %>
4
+ <div class="text-gray-400 text-sm"><%= f.object.hint %></div>
5
+ <% end %>
6
+ <div class="mt-1">
7
+ <% blocks = Spina::Blocks::Block.active.sorted %>
8
+ <% selected_ids = f.object.block_ids || [] %>
9
+ <div class="space-y-2">
10
+ <% blocks.each do |block| %>
11
+ <label class="flex items-center space-x-2 cursor-pointer">
12
+ <input type="checkbox"
13
+ name="<%= f.object_name %>[block_ids][]"
14
+ value="<%= block.id %>"
15
+ <%= 'checked' if selected_ids.include?(block.id) %>
16
+ class="form-checkbox rounded text-spina border-gray-300">
17
+ <span class="text-sm text-gray-700"><%= block.title %></span>
18
+ <span class="text-xs text-gray-400">(<%= block.block_template %>)</span>
19
+ </label>
20
+ <% end %>
21
+ </div>
22
+ <input type="hidden" name="<%= f.object_name %>[block_ids][]" value="">
23
+ </div>
24
+ </div>
@@ -0,0 +1,13 @@
1
+ <div class="mt-6">
2
+ <label class="block text-sm leading-5 font-medium text-gray-700"><%= f.object.title %></label>
3
+ <% if f.object.hint.present? %>
4
+ <div class="text-gray-400 text-sm"><%= f.object.hint %></div>
5
+ <% end %>
6
+ <div class="mt-1">
7
+ <% blocks = Spina::Blocks::Block.active.sorted %>
8
+ <%= f.select :block_id,
9
+ blocks.map { |b| [b.title, b.id] },
10
+ {include_blank: t('spina.blocks.select_block')},
11
+ class: "form-select text-sm w-full shadow-xs" %>
12
+ </div>
13
+ </div>
@@ -0,0 +1,4 @@
1
+ <%= render Spina::Pages::TabButtonComponent.new(tab_name: 'block_content') do %>
2
+ <%= heroicon('document-text', class: 'w-4 h-4 mr-1 -ml-1 opacity-75') %>
3
+ <%= t('spina.blocks.content') %>
4
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <%= render Spina::Pages::TabButtonComponent.new(tab_name: 'block_settings') do %>
2
+ <%= heroicon('cog-8-tooth', class: 'w-4 h-4 mr-1 -ml-1 opacity-75') %>
3
+ <%= t('spina.blocks.settings') %>
4
+ <% end %>
@@ -0,0 +1,53 @@
1
+ <div data-controller="tabs" data-tabs-active="cursor-default text-gray-900 bg-spina-dark/10" data-tabs-inactive="cursor-pointer bg-transparent text-gray-400 border-transparent">
2
+ <%= render Spina::UserInterface::HeaderComponent.new do |header| %>
3
+ <% header.with_actions do %>
4
+ <%= render Spina::UserInterface::TranslationsComponent.new(@block, label: @locale.to_s.upcase) %>
5
+
6
+ <div class="relative" data-controller="reveal" data-reveal-away-value>
7
+ <button type="button" class="btn btn-default px-3" data-action="reveal#toggle">
8
+ <svg class="w-5 h-5 text-gray-600" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
9
+ <path d="M6 10a2 2 0 11-4 0 2 2 0 014 0zM12 10a2 2 0 11-4 0 2 2 0 014 0zM16 12a2 2 0 100-4 2 2 0 000 4z" />
10
+ </svg>
11
+ </button>
12
+
13
+ <div hidden data-reveal data-transition class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg border border-gray-200 z-30">
14
+ <div class="rounded-md bg-white shadow-xs py-1">
15
+ <%= button_to t('spina.blocks.delete'),
16
+ spina.blocks_admin_block_path(@block),
17
+ method: :delete,
18
+ class: "block w-full text-left px-4 py-2 text-sm leading-5 font-medium text-red-500 cursor-pointer bg-white hover:bg-red-100/50 hover:text-red-500",
19
+ form: {data: {controller: "confirm", confirm_message: t('spina.blocks.delete_confirmation')}} %>
20
+ </div>
21
+ </div>
22
+ </div>
23
+
24
+ <button type="submit" form="<%= dom_id(@block) %>" class="btn btn-primary" data-controller="button hotkeys" data-hotkeys="command+s, ctrl+s" data-hotkeys-target="button" data-action="button#loading" data-loading-message="Saving...">
25
+ <%= heroicon('check', style: :mini, class: 'w-5 h-5 -ml-1 mr-1') %>
26
+ <%= t('spina.blocks.save') %>
27
+ </button>
28
+ <% end %>
29
+
30
+ <% header.with_navigation do %>
31
+ <nav class="-mb-3 mt-4">
32
+ <ul class="inline-flex w-auto rounded-md bg-white">
33
+ <% @tabs.each do |tab_name| %>
34
+ <%= render partial: "button_#{tab_name}" %>
35
+ <% end %>
36
+ </ul>
37
+ </nav>
38
+ <% end %>
39
+ <% end %>
40
+
41
+ <div class="p-4 md:p-8">
42
+ <%= form_with model: @block, url: spina.blocks_admin_block_path(@block), id: dom_id(@block) do |f| %>
43
+ <%= hidden_field_tag :locale, @locale %>
44
+ <% Mobility.with_locale(@locale) do %>
45
+ <% @tabs.each_with_index do |tab_name, idx| %>
46
+ <div data-tabs-target="pane" id="<%= tab_name %>" <%= 'hidden' unless idx.zero? %>>
47
+ <%= render partial: "form_#{tab_name}", locals: {f: f} %>
48
+ </div>
49
+ <% end %>
50
+ <% end %>
51
+ <% end %>
52
+ </div>
53
+ </div>
@@ -0,0 +1,8 @@
1
+ <div class="max-w-5xl">
2
+ <label class="font-medium text-gray-700">
3
+ <%= Spina::Blocks::Block.human_attribute_name(:title) %>
4
+ </label>
5
+ <%= render Spina::Forms::TextFieldComponent.new(f, :title, size: "lg") %>
6
+ <turbo-frame id="block_content" src="<%= spina.edit_content_blocks_admin_block_path(f.object, locale: @locale) %>">
7
+ </turbo-frame>
8
+ </div>
@@ -0,0 +1,24 @@
1
+ <div class="max-w-5xl grid grid-cols-1 md:grid-cols-3 gap-3 md:gap-6">
2
+ <div class="col-span-1 mt-6 md:mt-0">
3
+ <label class="block text-sm font-medium leading-5 text-gray-700">
4
+ <%= Spina::Blocks::Block.human_attribute_name(:block_template) %>
5
+ </label>
6
+ </div>
7
+
8
+ <div class="col-span-2 mt-1">
9
+ <span class="text-sm text-gray-700"><%= @block.block_template %></span>
10
+ </div>
11
+
12
+ <div class="col-span-1 mt-6 md:mt-0">
13
+ <label class="block text-sm font-medium leading-5 text-gray-700">
14
+ <%= Spina::Blocks::Block.human_attribute_name(:active) %>
15
+ </label>
16
+ <div class="text-sm leading-5">
17
+ <p class="text-gray-500"><%= t('spina.blocks.active_description') %></p>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="col-span-2 mt-1">
22
+ <%= render Spina::Forms::SwitchComponent.new(f, :active) %>
23
+ </div>
24
+ </div>
@@ -0,0 +1,29 @@
1
+ <%= turbo_frame_tag dom_id(@block, :new_block_form) do %>
2
+ <%= form_with model: @block, url: spina.blocks_admin_blocks_path, data: {turbo_frame: "_top"} do |f| %>
3
+ <%= f.hidden_field :block_template %>
4
+
5
+ <div class="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
6
+ <div class="sm:flex sm:items-start">
7
+ <div class="w-full">
8
+ <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
9
+ <%= t('spina.blocks.new') %>
10
+ <span class="text-gray-400 text-sm">(<%= f.object.block_template %>)</span>
11
+ </h3>
12
+
13
+ <div class="mt-3">
14
+ <%= render Spina::Forms::TextFieldComponent.new(f, :title, size: 'lg', autofocus: true) %>
15
+ </div>
16
+ </div>
17
+ </div>
18
+ </div>
19
+ <div class="px-4 pb-5 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
20
+ <button type="submit" class="btn btn-primary w-full sm:w-auto sm:ml-2">
21
+ <%= t('spina.blocks.create') %>
22
+ </button>
23
+
24
+ <button type="button" class="btn btn-default w-full sm:w-auto mt-2 sm:mt-0" data-action="modal#close">
25
+ <%= t('spina.ui.cancel') %>
26
+ </button>
27
+ </div>
28
+ <% end %>
29
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= render 'form' %>
@@ -0,0 +1,11 @@
1
+ <turbo-frame id="block_content">
2
+ <%= fields_for(@block) do |f| %>
3
+ <% f.fields_for "#{@locale}_content".to_sym, build_parts(f.object, @parts) do |ff| %>
4
+ <%= ff.hidden_field :type, value: ff.object.class %>
5
+ <%= ff.hidden_field :name %>
6
+ <%= ff.hidden_field :title %>
7
+
8
+ <%= render "spina/admin/parts/#{parts_partial_namespace(ff.object.class.to_s)}/form", f: ff %>
9
+ <% end %>
10
+ <% end %>
11
+ </turbo-frame>
@@ -0,0 +1,81 @@
1
+ <%= render Spina::UserInterface::HeaderComponent.new do |header| %>
2
+ <% header.with_actions do %>
3
+ <% block_templates = current_theme.try(:block_templates) || [] %>
4
+ <% if block_templates.any? %>
5
+ <div class="relative" data-controller="reveal" data-reveal-away-value>
6
+ <button type="button" class="btn btn-primary" data-action="reveal#toggle">
7
+ <%= heroicon('plus', style: :mini, class: 'w-4 h-4 mr-1 -ml-1') %>
8
+ <%= t('spina.blocks.new') %>
9
+ </button>
10
+
11
+ <div hidden data-reveal data-transition class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg border border-gray-200 z-30">
12
+ <div class="rounded-md bg-white shadow-xs py-1">
13
+ <% block_templates.each do |bt| %>
14
+ <%= link_to spina.new_blocks_admin_block_path(block_template: bt[:name]),
15
+ class: "block px-4 py-2 text-sm font-medium leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900",
16
+ data: {turbo_frame: "modal", action: "reveal#hide"} do %>
17
+ <%= bt[:title] %>
18
+ <% if bt[:description].present? %>
19
+ <span class="text-gray-400 text-xs block"><%= bt[:description] %></span>
20
+ <% end %>
21
+ <% end %>
22
+ <% end %>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ <% end %>
27
+ <% end %>
28
+
29
+ <% header.with_navigation do %>
30
+ <nav class="-mb-1 md:-mb-3 mt-4">
31
+ <ul class="inline-flex flex-wrap w-auto rounded-md bg-white">
32
+ <%= render Spina::UserInterface::TabLinkComponent.new(spina.blocks_admin_blocks_path, active: @current_block_template.nil?) do %>
33
+ <%= heroicon('squares-2x2', class: 'h-4 w-4 mr-1 -ml-1 opacity-75') %>
34
+ <%= t('spina.blocks.all') %>
35
+ <% end %>
36
+
37
+ <% @block_templates.each do |bt| %>
38
+ <%= render Spina::UserInterface::TabLinkComponent.new(
39
+ spina.blocks_admin_blocks_path(block_template: bt[:name]),
40
+ active: @current_block_template == bt[:name].to_s) do %>
41
+ <%= bt[:title] %>
42
+ <% end %>
43
+ <% end %>
44
+ </ul>
45
+ </nav>
46
+ <% end %>
47
+ <% end %>
48
+
49
+ <% if @blocks.any? %>
50
+ <div class="my-6 md:m-8 bg-white md:rounded-lg border-l-0 border-r-0 border md:border-r md:border-l border-gray-200 border-b-0 shadow-xs">
51
+ <div data-controller="sortable" data-sortable-url="<%= spina.sort_blocks_admin_blocks_path %>">
52
+ <form data-sortable-target="form" method="post" action="<%= spina.sort_blocks_admin_blocks_path %>">
53
+ <input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
54
+ </form>
55
+ <ul data-sortable-target="list">
56
+ <% @blocks.each do |block| %>
57
+ <li class="border-b border-gray-200 flex items-center" data-id="<%= block.id %>">
58
+ <div class="px-4 py-1 cursor-move text-gray-300 hover:text-gray-500" data-sortable-handle>
59
+ <%= heroicon('bars-3', style: :mini, class: 'w-5 h-5') %>
60
+ </div>
61
+ <div class="flex-1 py-3 pr-4">
62
+ <%= link_to spina.edit_blocks_admin_block_path(block), class: "block" do %>
63
+ <div class="text-gray-900 font-medium text-sm"><%= block.title %></div>
64
+ <div class="text-gray-400 text-xs">
65
+ <%= block.block_template %>
66
+ <% unless block.active? %>
67
+ &middot; <span class="text-red-400"><%= t('spina.blocks.inactive') %></span>
68
+ <% end %>
69
+ </div>
70
+ <% end %>
71
+ </div>
72
+ </li>
73
+ <% end %>
74
+ </ul>
75
+ </div>
76
+ </div>
77
+ <% else %>
78
+ <div class="m-8 my-6 italic text-gray-400">
79
+ <%= t('spina.blocks.no_blocks_yet') %>
80
+ </div>
81
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= render(Spina::UserInterface::ModalComponent.new) do %>
2
+ <%= render 'new_block_form' %>
3
+ <% end %>
@@ -0,0 +1,90 @@
1
+ <%= render Spina::UserInterface::HeaderComponent.new do |header| %>
2
+ <% header.with_actions do %>
3
+ <% if @available_blocks.any? %>
4
+ <div class="relative" data-controller="reveal" data-reveal-away-value>
5
+ <button type="button" class="btn btn-primary" data-action="reveal#toggle">
6
+ <%= heroicon('plus', style: :mini, class: 'w-4 h-4 mr-1 -ml-1') %>
7
+ <%= t('spina.page_blocks.add_block') %>
8
+ </button>
9
+
10
+ <div hidden data-reveal data-transition class="origin-top-right absolute right-0 mt-2 w-72 max-h-96 overflow-y-auto rounded-md shadow-lg border border-gray-200 z-30">
11
+ <div class="rounded-md bg-white shadow-xs py-1">
12
+ <% Spina::Blocks::Category.sorted.each do |category| %>
13
+ <% category_blocks = @available_blocks.where(category: category) %>
14
+ <% if category_blocks.any? %>
15
+ <div class="px-4 py-1 text-xs font-semibold text-gray-400 uppercase tracking-wider mt-1"><%= category.label %></div>
16
+ <% category_blocks.each do |block| %>
17
+ <%= button_to spina.blocks_admin_page_page_blocks_path(page_id: @page.id),
18
+ params: {page_block: {block_id: block.id}},
19
+ method: :post,
20
+ class: "block w-full text-left px-4 py-2 text-sm font-medium leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900" do %>
21
+ <div><%= block.title %></div>
22
+ <div class="text-xs text-gray-400"><%= block.block_template %></div>
23
+ <% end %>
24
+ <% end %>
25
+ <% end %>
26
+ <% end %>
27
+
28
+ <% uncategorized = @available_blocks.where(category_id: nil) %>
29
+ <% if uncategorized.any? %>
30
+ <div class="px-4 py-1 text-xs font-semibold text-gray-400 uppercase tracking-wider mt-1"><%= t('spina.blocks.uncategorized') %></div>
31
+ <% uncategorized.each do |block| %>
32
+ <%= button_to spina.blocks_admin_page_page_blocks_path(page_id: @page.id),
33
+ params: {page_block: {block_id: block.id}},
34
+ method: :post,
35
+ class: "block w-full text-left px-4 py-2 text-sm font-medium leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900" do %>
36
+ <div><%= block.title %></div>
37
+ <div class="text-xs text-gray-400"><%= block.block_template %></div>
38
+ <% end %>
39
+ <% end %>
40
+ <% end %>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ <% end %>
45
+
46
+ <%= link_to spina.edit_admin_page_path(@page), class: "btn btn-default" do %>
47
+ <%= heroicon('arrow-left', style: :mini, class: 'w-4 h-4 mr-1 -ml-1') %>
48
+ <%= t('spina.page_blocks.back_to_page') %>
49
+ <% end %>
50
+ <% end %>
51
+ <% end %>
52
+
53
+ <% if @page_blocks.any? %>
54
+ <div class="my-6 md:m-8 bg-white md:rounded-lg border-l-0 border-r-0 border md:border-r md:border-l border-gray-200 border-b-0 shadow-xs">
55
+ <div data-controller="sortable" data-sortable-url="<%= spina.sort_blocks_admin_page_page_blocks_path(page_id: @page.id) %>">
56
+ <form data-sortable-target="form" method="post" action="<%= spina.sort_blocks_admin_page_page_blocks_path(page_id: @page.id) %>">
57
+ <input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
58
+ </form>
59
+ <ul data-sortable-target="list">
60
+ <% @page_blocks.each do |page_block| %>
61
+ <li class="border-b border-gray-200 flex items-center" data-id="<%= page_block.id %>">
62
+ <div class="px-4 py-1 cursor-move text-gray-300 hover:text-gray-500" data-sortable-handle>
63
+ <%= heroicon('bars-3', style: :mini, class: 'w-5 h-5') %>
64
+ </div>
65
+ <div class="flex-1 py-3">
66
+ <%= link_to spina.edit_blocks_admin_block_path(page_block.block), class: "block" do %>
67
+ <div class="text-gray-900 font-medium text-sm"><%= page_block.block.title %></div>
68
+ <div class="text-gray-400 text-xs">
69
+ <%= page_block.block.block_template %>
70
+ </div>
71
+ <% end %>
72
+ </div>
73
+ <div class="pr-4">
74
+ <%= button_to spina.blocks_admin_page_page_block_path(page_id: @page.id, id: page_block.id),
75
+ method: :delete,
76
+ class: "text-gray-300 hover:text-red-500 transition-colors",
77
+ form: {data: {controller: "confirm", confirm_message: t('spina.page_blocks.remove_confirmation')}} do %>
78
+ <%= heroicon('x-mark', style: :mini, class: 'w-5 h-5') %>
79
+ <% end %>
80
+ </div>
81
+ </li>
82
+ <% end %>
83
+ </ul>
84
+ </div>
85
+ </div>
86
+ <% else %>
87
+ <div class="m-8 my-6 italic text-gray-400">
88
+ <%= t('spina.page_blocks.no_blocks_yet') %>
89
+ </div>
90
+ <% end %>
@@ -0,0 +1,37 @@
1
+ en:
2
+ spina:
3
+ blocks:
4
+ title: "Blocks"
5
+ new: "New block"
6
+ create: "Create block"
7
+ save: "Save block"
8
+ saved: "Block saved"
9
+ deleted: "Block deleted"
10
+ delete: "Delete block"
11
+ delete_confirmation: "Are you sure you want to delete this block?"
12
+ couldnt_be_saved: "Block couldn't be saved"
13
+ sorting_saved: "Sorting saved"
14
+ content: "Content"
15
+ settings: "Settings"
16
+ all: "All"
17
+ category: "Category"
18
+ no_category: "— No category —"
19
+ select_block: "— Select block —"
20
+ no_blocks_yet: "No blocks yet. Create your first block to get started."
21
+ inactive: "Inactive"
22
+ uncategorized: "Uncategorized"
23
+ active_description: "Inactive blocks are hidden from pages."
24
+ block_categories:
25
+ saved: "Category saved"
26
+ couldnt_be_saved: "Category couldn't be saved"
27
+ page_blocks:
28
+ title: "Page Blocks"
29
+ add_block: "Add block"
30
+ back_to_page: "Back to page"
31
+ added: "Block added to page"
32
+ couldnt_be_added: "Block couldn't be added"
33
+ removed: "Block removed from page"
34
+ sorting_saved: "Sorting saved"
35
+ remove_confirmation: "Remove this block from the page?"
36
+ no_blocks_yet: "No blocks on this page yet. Add blocks from the library to build your page."
37
+ manage_blocks: "Manage blocks"
data/config/routes.rb ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ Spina::Engine.routes.draw do
4
+ namespace :blocks, path: '', module: 'blocks' do
5
+ namespace :admin, path: Spina.config.backend_path do
6
+ resources :blocks do
7
+ member do
8
+ get :edit_content
9
+ end
10
+
11
+ collection do
12
+ post :sort
13
+ end
14
+ end
15
+
16
+ resources :categories, only: %i[index edit update]
17
+
18
+ resources :pages, only: [] do
19
+ resources :page_blocks, only: %i[index create destroy] do
20
+ collection do
21
+ post :sort
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSpinaBlocksCategories < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :spina_blocks_categories do |t|
6
+ t.string :name, null: false
7
+ t.string :label, null: false
8
+ t.integer :position, default: 0
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :spina_blocks_categories, :name, unique: true
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSpinaBlocksBlocks < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :spina_blocks_blocks do |t|
6
+ t.string :title, null: false
7
+ t.string :name, null: false
8
+ t.string :block_template, null: false
9
+ t.references :category, foreign_key: { to_table: :spina_blocks_categories }, null: true
10
+ t.integer :position, default: 0
11
+ t.boolean :active, default: true
12
+ t.json :json_attributes
13
+
14
+ t.timestamps
15
+ end
16
+
17
+ add_index :spina_blocks_blocks, :name, unique: true
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSpinaBlocksPageBlocks < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :spina_blocks_page_blocks do |t|
6
+ t.references :page, null: false, foreign_key: { to_table: :spina_pages }
7
+ t.references :block, null: false, foreign_key: { to_table: :spina_blocks_blocks }
8
+ t.integer :position, default: 0
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :spina_blocks_page_blocks, %i[page_id block_id], unique: true
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RemoveNameFromSpinaBlocksBlocks < ActiveRecord::Migration[7.0]
4
+ def change
5
+ remove_index :spina_blocks_blocks, :name
6
+ remove_column :spina_blocks_blocks, :name, :string
7
+ end
8
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Blocks
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace Spina::Blocks
7
+
8
+ config.before_initialize do
9
+ ::Spina::Plugin.register do |plugin|
10
+ plugin.name = 'blocks'
11
+ plugin.namespace = 'blocks'
12
+ end
13
+
14
+ ::Spina::Theme.class_eval do
15
+ attr_accessor :block_templates, :block_categories
16
+
17
+ unless method_defined?(:initialize_without_blocks)
18
+ alias_method :initialize_without_blocks, :initialize
19
+
20
+ def initialize
21
+ initialize_without_blocks
22
+ @block_templates = []
23
+ @block_categories = []
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ initializer 'spina.blocks.append_migrations' do |app|
30
+ unless app.root.to_s.match?(root.to_s)
31
+ config.paths['db/migrate'].expanded.each do |expanded_path|
32
+ app.config.paths['db/migrate'] << expanded_path
33
+ end
34
+ end
35
+ end
36
+
37
+ initializer 'spina.blocks.register_parts' do
38
+ config.to_prepare do
39
+ ::Spina::Part.register(
40
+ Spina::Parts::BlockReference,
41
+ Spina::Parts::BlockCollection
42
+ )
43
+ end
44
+ end
45
+
46
+ initializer 'spina.blocks.extend_page' do
47
+ config.to_prepare do
48
+ Dir.glob(Spina::Blocks::Engine.root.join('app/overrides/**/*.rb')).sort.each do |override|
49
+ load override
50
+ end
51
+ end
52
+ end
53
+
54
+ initializer 'spina.blocks.tailwind_content' do
55
+ ::Spina.config.tailwind_content << "#{Spina::Blocks::Engine.root}/app/views/**/*.*"
56
+ ::Spina.config.tailwind_content << "#{Spina::Blocks::Engine.root}/app/helpers/**/*.*"
57
+ end
58
+
59
+ initializer 'spina.blocks.i18n' do
60
+ config.i18n.load_path += Dir[Spina::Blocks::Engine.root.join('config', 'locales', '*.{rb,yml}')]
61
+ end
62
+
63
+ initializer 'spina.blocks.helpers' do
64
+ config.to_prepare do
65
+ ActionView::Base.include Spina::Blocks::BlocksHelper
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Blocks
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spina'
4
+ require 'spina/blocks/version'
5
+ require 'spina/blocks/engine'
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: spina-blocks
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Konstantin Kanashchuk
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: spina
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ description: A plugin for Spina CMS that adds reusable block components that can be
27
+ assembled into pages.
28
+ email:
29
+ - ksnitsky@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - README.md
35
+ - Rakefile
36
+ - app/controllers/spina/blocks/admin/blocks_controller.rb
37
+ - app/controllers/spina/blocks/admin/categories_controller.rb
38
+ - app/controllers/spina/blocks/admin/page_blocks_controller.rb
39
+ - app/helpers/spina/blocks/blocks_helper.rb
40
+ - app/models/spina/blocks/application_record.rb
41
+ - app/models/spina/blocks/block.rb
42
+ - app/models/spina/blocks/category.rb
43
+ - app/models/spina/blocks/page_block.rb
44
+ - app/models/spina/parts/block_collection.rb
45
+ - app/models/spina/parts/block_reference.rb
46
+ - app/overrides/spina/account_override.rb
47
+ - app/overrides/spina/page_override.rb
48
+ - app/views/spina/admin/hooks/blocks/_website_secondary_navigation.html.erb
49
+ - app/views/spina/admin/parts/block_collections/_form.html.erb
50
+ - app/views/spina/admin/parts/block_references/_form.html.erb
51
+ - app/views/spina/blocks/admin/blocks/_button_block_content.html.erb
52
+ - app/views/spina/blocks/admin/blocks/_button_block_settings.html.erb
53
+ - app/views/spina/blocks/admin/blocks/_form.html.erb
54
+ - app/views/spina/blocks/admin/blocks/_form_block_content.html.erb
55
+ - app/views/spina/blocks/admin/blocks/_form_block_settings.html.erb
56
+ - app/views/spina/blocks/admin/blocks/_new_block_form.html.erb
57
+ - app/views/spina/blocks/admin/blocks/edit.html.erb
58
+ - app/views/spina/blocks/admin/blocks/edit_content.html.erb
59
+ - app/views/spina/blocks/admin/blocks/index.html.erb
60
+ - app/views/spina/blocks/admin/blocks/new.html.erb
61
+ - app/views/spina/blocks/admin/page_blocks/index.html.erb
62
+ - config/locales/en.yml
63
+ - config/routes.rb
64
+ - db/migrate/1_create_spina_blocks_categories.rb
65
+ - db/migrate/2_create_spina_blocks_blocks.rb
66
+ - db/migrate/3_create_spina_blocks_page_blocks.rb
67
+ - db/migrate/4_remove_name_from_spina_blocks_blocks.rb
68
+ - lib/spina-blocks.rb
69
+ - lib/spina/blocks/engine.rb
70
+ - lib/spina/blocks/version.rb
71
+ homepage: https://github.com/ksnitsky/spina-blocks
72
+ licenses:
73
+ - MIT
74
+ metadata:
75
+ rubygems_mfa_required: 'true'
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 2.7.0
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubygems_version: 4.0.3
91
+ specification_version: 4
92
+ summary: Block component library for Spina CMS
93
+ test_files: []