spree_page_builder 5.3.0.rc1

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 (219) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +43 -0
  3. data/Rakefile +18 -0
  4. data/app/controllers/concerns/spree/admin/page_builder_concern.rb +45 -0
  5. data/app/controllers/concerns/spree/admin/storefront_breadcrumb_concern.rb +12 -0
  6. data/app/controllers/spree/admin/page_blocks_controller.rb +58 -0
  7. data/app/controllers/spree/admin/page_links_controller.rb +60 -0
  8. data/app/controllers/spree/admin/page_sections_controller.rb +82 -0
  9. data/app/controllers/spree/admin/pages_controller.rb +39 -0
  10. data/app/controllers/spree/admin/storefront_controller.rb +41 -0
  11. data/app/controllers/spree/admin/themes_controller.rb +87 -0
  12. data/app/helpers/spree/admin/page_builder_helper.rb +125 -0
  13. data/app/helpers/spree/admin/themes_helper.rb +9 -0
  14. data/app/jobs/spree/page_builder/products/touch_taxons_job_decorator.rb +15 -0
  15. data/app/jobs/spree/themes/duplicate_components_job.rb +59 -0
  16. data/app/jobs/spree/themes/screenshot_job.rb +81 -0
  17. data/app/models/concerns/spree/has_page_links.rb +53 -0
  18. data/app/models/spree/page.rb +188 -0
  19. data/app/models/spree/page_block.rb +73 -0
  20. data/app/models/spree/page_blocks/buttons.rb +29 -0
  21. data/app/models/spree/page_blocks/heading.rb +18 -0
  22. data/app/models/spree/page_blocks/image.rb +20 -0
  23. data/app/models/spree/page_blocks/link.rb +21 -0
  24. data/app/models/spree/page_blocks/mega_nav.rb +33 -0
  25. data/app/models/spree/page_blocks/mega_nav_with_subcategories.rb +32 -0
  26. data/app/models/spree/page_blocks/metafields.rb +18 -0
  27. data/app/models/spree/page_blocks/nav.rb +15 -0
  28. data/app/models/spree/page_blocks/newsletter_form.rb +18 -0
  29. data/app/models/spree/page_blocks/products/brand.rb +15 -0
  30. data/app/models/spree/page_blocks/products/buy_buttons.rb +24 -0
  31. data/app/models/spree/page_blocks/products/description.rb +18 -0
  32. data/app/models/spree/page_blocks/products/price.rb +18 -0
  33. data/app/models/spree/page_blocks/products/quantity_selector.rb +20 -0
  34. data/app/models/spree/page_blocks/products/share.rb +8 -0
  35. data/app/models/spree/page_blocks/products/title.rb +19 -0
  36. data/app/models/spree/page_blocks/products/variant_picker.rb +13 -0
  37. data/app/models/spree/page_blocks/subheading.rb +17 -0
  38. data/app/models/spree/page_blocks/text.rb +16 -0
  39. data/app/models/spree/page_builder/policy_decorator.rb +17 -0
  40. data/app/models/spree/page_builder/post_decorator.rb +17 -0
  41. data/app/models/spree/page_builder/product_decorator.rb +17 -0
  42. data/app/models/spree/page_builder/store_decorator.rb +46 -0
  43. data/app/models/spree/page_builder/taxon_decorator.rb +45 -0
  44. data/app/models/spree/page_link.rb +60 -0
  45. data/app/models/spree/page_section.rb +222 -0
  46. data/app/models/spree/page_sections/announcement_bar.rb +28 -0
  47. data/app/models/spree/page_sections/breadcrumbs.rb +12 -0
  48. data/app/models/spree/page_sections/collection_banner.rb +18 -0
  49. data/app/models/spree/page_sections/custom_code.rb +11 -0
  50. data/app/models/spree/page_sections/featured_posts.rb +45 -0
  51. data/app/models/spree/page_sections/featured_product.rb +50 -0
  52. data/app/models/spree/page_sections/featured_taxon.rb +90 -0
  53. data/app/models/spree/page_sections/featured_taxons.rb +45 -0
  54. data/app/models/spree/page_sections/footer.rb +101 -0
  55. data/app/models/spree/page_sections/header.rb +62 -0
  56. data/app/models/spree/page_sections/image_banner.rb +55 -0
  57. data/app/models/spree/page_sections/image_with_text.rb +65 -0
  58. data/app/models/spree/page_sections/main_password_footer.rb +18 -0
  59. data/app/models/spree/page_sections/main_password_header.rb +20 -0
  60. data/app/models/spree/page_sections/newsletter.rb +54 -0
  61. data/app/models/spree/page_sections/page_title.rb +19 -0
  62. data/app/models/spree/page_sections/post_details.rb +19 -0
  63. data/app/models/spree/page_sections/post_grid.rb +19 -0
  64. data/app/models/spree/page_sections/product_details.rb +53 -0
  65. data/app/models/spree/page_sections/product_grid.rb +13 -0
  66. data/app/models/spree/page_sections/related_products.rb +58 -0
  67. data/app/models/spree/page_sections/rich_text.rb +31 -0
  68. data/app/models/spree/page_sections/taxon_banner.rb +18 -0
  69. data/app/models/spree/page_sections/taxon_grid.rb +17 -0
  70. data/app/models/spree/page_sections/video.rb +107 -0
  71. data/app/models/spree/pages/account.rb +19 -0
  72. data/app/models/spree/pages/cart.rb +19 -0
  73. data/app/models/spree/pages/checkout.rb +15 -0
  74. data/app/models/spree/pages/custom.rb +38 -0
  75. data/app/models/spree/pages/homepage.rb +72 -0
  76. data/app/models/spree/pages/login.rb +19 -0
  77. data/app/models/spree/pages/password.rb +59 -0
  78. data/app/models/spree/pages/post.rb +27 -0
  79. data/app/models/spree/pages/post_list.rb +36 -0
  80. data/app/models/spree/pages/product_details.rb +30 -0
  81. data/app/models/spree/pages/search_results.rb +43 -0
  82. data/app/models/spree/pages/shop_all.rb +40 -0
  83. data/app/models/spree/pages/taxon.rb +29 -0
  84. data/app/models/spree/pages/taxon_list.rb +41 -0
  85. data/app/models/spree/pages/wishlist.rb +15 -0
  86. data/app/models/spree/theme.rb +233 -0
  87. data/app/models/spree/themes/default.rb +97 -0
  88. data/app/services/spree/taxons/touch_featured_sections.rb +21 -0
  89. data/app/views/layouts/spree/page_builder.html.erb +46 -0
  90. data/app/views/spree/admin/dashboard/_store_preview.html.erb +38 -0
  91. data/app/views/spree/admin/page_blocks/_form_tab_buttons.html.erb +5 -0
  92. data/app/views/spree/admin/page_blocks/create.turbo_stream.erb +4 -0
  93. data/app/views/spree/admin/page_blocks/destroy.turbo_stream.erb +9 -0
  94. data/app/views/spree/admin/page_blocks/edit.html.erb +43 -0
  95. data/app/views/spree/admin/page_blocks/forms/_brand.html.erb +15 -0
  96. data/app/views/spree/admin/page_blocks/forms/_brand_logo.html.erb +4 -0
  97. data/app/views/spree/admin/page_blocks/forms/_buttons.html.erb +13 -0
  98. data/app/views/spree/admin/page_blocks/forms/_heading.html.erb +28 -0
  99. data/app/views/spree/admin/page_blocks/forms/_image.html.erb +10 -0
  100. data/app/views/spree/admin/page_blocks/forms/_link.html.erb +19 -0
  101. data/app/views/spree/admin/page_blocks/forms/_mega_nav.html.erb +14 -0
  102. data/app/views/spree/admin/page_blocks/forms/_mega_nav_with_subcategories.html.erb +9 -0
  103. data/app/views/spree/admin/page_blocks/forms/_metafields.html.erb +35 -0
  104. data/app/views/spree/admin/page_blocks/forms/_nav.html.erb +6 -0
  105. data/app/views/spree/admin/page_blocks/forms/_newsletter_form.html.erb +20 -0
  106. data/app/views/spree/admin/page_blocks/forms/_price.html.erb +15 -0
  107. data/app/views/spree/admin/page_blocks/forms/_share.html.erb +5 -0
  108. data/app/views/spree/admin/page_blocks/forms/_subheading.html.erb +26 -0
  109. data/app/views/spree/admin/page_blocks/forms/_text.html.erb +21 -0
  110. data/app/views/spree/admin/page_blocks/forms/_title.html.erb +20 -0
  111. data/app/views/spree/admin/page_blocks/forms/_variant_picker.html.erb +10 -0
  112. data/app/views/spree/admin/page_blocks/forms/products/_buy_buttons.html.erb +10 -0
  113. data/app/views/spree/admin/page_blocks/forms/products/_description.html.erb +0 -0
  114. data/app/views/spree/admin/page_blocks/forms/products/_quantity_selector.html.erb +10 -0
  115. data/app/views/spree/admin/page_blocks/move_higher.turbo_stream.erb +4 -0
  116. data/app/views/spree/admin/page_blocks/move_lower.turbo_stream.erb +4 -0
  117. data/app/views/spree/admin/page_blocks/show.html.erb +1 -0
  118. data/app/views/spree/admin/page_blocks/update.turbo_stream.erb +6 -0
  119. data/app/views/spree/admin/page_builder/_add_block.html.erb +22 -0
  120. data/app/views/spree/admin/page_builder/_color_palette.html.erb +17 -0
  121. data/app/views/spree/admin/page_builder/_color_picker.html.erb +26 -0
  122. data/app/views/spree/admin/page_builder/_header.html.erb +113 -0
  123. data/app/views/spree/admin/page_builder/_labeled_range_input.html.erb +10 -0
  124. data/app/views/spree/admin/page_builder/_pages_dropdown.html.erb +17 -0
  125. data/app/views/spree/admin/page_builder/_range_input.html.erb +12 -0
  126. data/app/views/spree/admin/page_builder/_sidebar.html.erb +12 -0
  127. data/app/views/spree/admin/page_builder/_sidebar_block.html.erb +30 -0
  128. data/app/views/spree/admin/page_builder/_sidebar_colors.html.erb +86 -0
  129. data/app/views/spree/admin/page_builder/_sidebar_fonts.html.erb +85 -0
  130. data/app/views/spree/admin/page_builder/_sidebar_section.html.erb +44 -0
  131. data/app/views/spree/admin/page_builder/_sidebar_sections.html.erb +38 -0
  132. data/app/views/spree/admin/page_builder/_sidebar_sections_toolbar.html.erb +180 -0
  133. data/app/views/spree/admin/page_links/_form.html.erb +10 -0
  134. data/app/views/spree/admin/page_links/_linkable_type_dropdown.html.erb +15 -0
  135. data/app/views/spree/admin/page_links/_list.html.erb +36 -0
  136. data/app/views/spree/admin/page_links/_refresh_theme_preview.turbo_stream.erb +5 -0
  137. data/app/views/spree/admin/page_links/create.turbo_stream.erb +10 -0
  138. data/app/views/spree/admin/page_links/destroy.turbo_stream.erb +15 -0
  139. data/app/views/spree/admin/page_links/edit.html.erb +27 -0
  140. data/app/views/spree/admin/page_links/update.turbo_stream.erb +14 -0
  141. data/app/views/spree/admin/page_sections/_form_tab_buttons.html.erb +8 -0
  142. data/app/views/spree/admin/page_sections/create.turbo_stream.erb +11 -0
  143. data/app/views/spree/admin/page_sections/destroy.turbo_stream.erb +16 -0
  144. data/app/views/spree/admin/page_sections/edit.html.erb +39 -0
  145. data/app/views/spree/admin/page_sections/fields/_background_color.html.erb +6 -0
  146. data/app/views/spree/admin/page_sections/fields/_border_bottom_width.html.erb +5 -0
  147. data/app/views/spree/admin/page_sections/fields/_border_color.html.erb +7 -0
  148. data/app/views/spree/admin/page_sections/fields/_border_top_width.html.erb +6 -0
  149. data/app/views/spree/admin/page_sections/fields/_bottom_padding.html.erb +5 -0
  150. data/app/views/spree/admin/page_sections/fields/_button.html.erb +15 -0
  151. data/app/views/spree/admin/page_sections/fields/_header_layout.html.erb +26 -0
  152. data/app/views/spree/admin/page_sections/fields/_height.html.erb +6 -0
  153. data/app/views/spree/admin/page_sections/fields/_text_color.html.erb +7 -0
  154. data/app/views/spree/admin/page_sections/fields/_top_padding.html.erb +6 -0
  155. data/app/views/spree/admin/page_sections/forms/_announcement_bar.html.erb +5 -0
  156. data/app/views/spree/admin/page_sections/forms/_brand_story.html.erb +0 -0
  157. data/app/views/spree/admin/page_sections/forms/_breadcrumbs.html.erb +0 -0
  158. data/app/views/spree/admin/page_sections/forms/_collection_banner.html.erb +0 -0
  159. data/app/views/spree/admin/page_sections/forms/_custom_code.html.erb +4 -0
  160. data/app/views/spree/admin/page_sections/forms/_featured_posts.html.erb +33 -0
  161. data/app/views/spree/admin/page_sections/forms/_featured_product.html.erb +4 -0
  162. data/app/views/spree/admin/page_sections/forms/_featured_taxon.html.erb +59 -0
  163. data/app/views/spree/admin/page_sections/forms/_featured_taxons.html.erb +13 -0
  164. data/app/views/spree/admin/page_sections/forms/_footer.html.erb +20 -0
  165. data/app/views/spree/admin/page_sections/forms/_header.html.erb +12 -0
  166. data/app/views/spree/admin/page_sections/forms/_image_banner.html.erb +13 -0
  167. data/app/views/spree/admin/page_sections/forms/_image_with_text.html.erb +28 -0
  168. data/app/views/spree/admin/page_sections/forms/_main_password_footer.html.erb +9 -0
  169. data/app/views/spree/admin/page_sections/forms/_main_password_header.erb +13 -0
  170. data/app/views/spree/admin/page_sections/forms/_newsletter.html.erb +15 -0
  171. data/app/views/spree/admin/page_sections/forms/_page_title.html.erb +4 -0
  172. data/app/views/spree/admin/page_sections/forms/_post_details.html.erb +0 -0
  173. data/app/views/spree/admin/page_sections/forms/_post_grid.html.erb +0 -0
  174. data/app/views/spree/admin/page_sections/forms/_product_details.html.erb +1 -0
  175. data/app/views/spree/admin/page_sections/forms/_product_grid.html.erb +1 -0
  176. data/app/views/spree/admin/page_sections/forms/_related_products.html.erb +32 -0
  177. data/app/views/spree/admin/page_sections/forms/_rich_text.html.erb +0 -0
  178. data/app/views/spree/admin/page_sections/forms/_taxon_banner.html.erb +0 -0
  179. data/app/views/spree/admin/page_sections/forms/_taxon_grid.html.erb +5 -0
  180. data/app/views/spree/admin/page_sections/forms/_video.html.erb +15 -0
  181. data/app/views/spree/admin/page_sections/move_higher.turbo_stream.erb +7 -0
  182. data/app/views/spree/admin/page_sections/move_lower.turbo_stream.erb +7 -0
  183. data/app/views/spree/admin/page_sections/new.html.erb +26 -0
  184. data/app/views/spree/admin/page_sections/remove_attachment.turbo_stream.erb +4 -0
  185. data/app/views/spree/admin/page_sections/restore_design_settings_to_defaults.turbo_stream.erb +4 -0
  186. data/app/views/spree/admin/page_sections/show.html.erb +1 -0
  187. data/app/views/spree/admin/page_sections/update.turbo_stream.erb +10 -0
  188. data/app/views/spree/admin/pages/_form.html.erb +20 -0
  189. data/app/views/spree/admin/pages/_table_header.html.erb +6 -0
  190. data/app/views/spree/admin/pages/_table_row.html.erb +28 -0
  191. data/app/views/spree/admin/pages/edit.html.erb +13 -0
  192. data/app/views/spree/admin/pages/filters.html.erb +7 -0
  193. data/app/views/spree/admin/pages/index.html.erb +11 -0
  194. data/app/views/spree/admin/pages/new.html.erb +1 -0
  195. data/app/views/spree/admin/storefront/edit.html.erb +89 -0
  196. data/app/views/spree/admin/themes/_theme.html.erb +56 -0
  197. data/app/views/spree/admin/themes/_theme_preview_image.html.erb +17 -0
  198. data/app/views/spree/admin/themes/edit.html.erb +14 -0
  199. data/app/views/spree/admin/themes/index.html.erb +80 -0
  200. data/app/views/spree/admin/themes/update.turbo_stream.erb +1 -0
  201. data/config/initializers/spree_admin_navigation.rb +46 -0
  202. data/config/initializers/spree_admin_partials.rb +3 -0
  203. data/config/locales/en.yml +42 -0
  204. data/config/routes.rb +43 -0
  205. data/db/migrate/20250120094216_create_page_builder_models.rb +78 -0
  206. data/db/migrate/20250305121352_remove_page_builder_indices.rb +11 -0
  207. data/db/migrate/20250825175217_add_missing_page_builder_indexes.rb +7 -0
  208. data/db/migrate/20250913130044_add_page_links_counter_cache_to_spree_stores.rb +10 -0
  209. data/lib/generators/spree/page_builder/install/install_generator.rb +23 -0
  210. data/lib/spree/page_builder/engine.rb +185 -0
  211. data/lib/spree/page_builder/testing_support/factories/page_block_factory.rb +22 -0
  212. data/lib/spree/page_builder/testing_support/factories/page_factory.rb +33 -0
  213. data/lib/spree/page_builder/testing_support/factories/page_link_factory.rb +7 -0
  214. data/lib/spree/page_builder/testing_support/factories/page_section_factory.rb +27 -0
  215. data/lib/spree/page_builder/testing_support/factories/theme_factory.rb +14 -0
  216. data/lib/spree/page_builder/testing_support/factories.rb +3 -0
  217. data/lib/spree/page_builder.rb +4 -0
  218. data/lib/spree_page_builder.rb +1 -0
  219. metadata +288 -0
@@ -0,0 +1,40 @@
1
+ module Spree
2
+ module Pages
3
+ class ShopAll < Spree::Page
4
+ def icon_name
5
+ 'shopping-bag'
6
+ end
7
+
8
+ def page_builder_url
9
+ return unless page_builder_url_exists?(:products_path)
10
+
11
+ Spree::Core::Engine.routes.url_helpers.products_path
12
+ end
13
+
14
+ def preview_url(theme_preview = nil, page_preview = nil)
15
+ return unless page_builder_url_exists?(:products_path)
16
+
17
+ Spree::Core::Engine.routes.url_helpers.products_path(
18
+ theme_id: theme.id,
19
+ page_preview_id: page_preview&.id,
20
+ theme_preview_id: theme_preview&.id
21
+ )
22
+ end
23
+
24
+ def default_sections
25
+ [
26
+ Spree::PageSections::PageTitle.new(preferred_title: Spree.t(:shop_all)),
27
+ Spree::PageSections::ProductGrid.new
28
+ ]
29
+ end
30
+
31
+ def customizable?
32
+ true
33
+ end
34
+
35
+ def linkable?
36
+ true
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,29 @@
1
+ module Spree
2
+ module Pages
3
+ class Taxon < Spree::Page
4
+ def icon_name
5
+ 'bookmark'
6
+ end
7
+
8
+ def page_builder_url
9
+ return unless page_builder_url_exists?(:nested_taxons_path)
10
+
11
+ taxon = Spree::Taxon.first
12
+ return if taxon.nil?
13
+
14
+ Spree::Core::Engine.routes.url_helpers.nested_taxons_path(taxon)
15
+ end
16
+
17
+ def default_sections
18
+ [
19
+ Spree::PageSections::TaxonBanner.new,
20
+ Spree::PageSections::ProductGrid.new,
21
+ ]
22
+ end
23
+
24
+ def customizable?
25
+ true
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,41 @@
1
+ module Spree
2
+ module Pages
3
+ class TaxonList < Spree::Page
4
+ DISPLAY_NAME = Spree.t(:taxonomy_brands_name).freeze
5
+
6
+
7
+ def page_builder_url
8
+ return unless page_builder_url_exists?(:taxonomy_path)
9
+
10
+ Spree::Core::Engine.routes.url_helpers.taxonomy_path(taxonomy.id)
11
+ end
12
+
13
+ def icon_name
14
+ 'sort-a-z'
15
+ end
16
+
17
+ def default_sections
18
+ [
19
+ Spree::PageSections::TaxonGrid.new,
20
+ ]
21
+ end
22
+
23
+ def customizable?
24
+ false
25
+ end
26
+
27
+ def display_name
28
+ DISPLAY_NAME
29
+ end
30
+
31
+ # FIXME: this should use preferences
32
+ def taxonomy_id
33
+ store.taxonomies.first&.id
34
+ end
35
+
36
+ def taxonomy
37
+ @taxonomy ||= store.taxonomies.find(taxonomy_id)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ module Spree
2
+ module Pages
3
+ class Wishlist < Spree::Page
4
+ def page_builder_url
5
+ return unless page_builder_url_exists?(:account_wishlist_path)
6
+
7
+ Spree::Core::Engine.routes.url_helpers.account_wishlist_path
8
+ end
9
+
10
+ def linkable?
11
+ true
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,233 @@
1
+ module Spree
2
+ class Theme < Spree.base_class
3
+ include Spree::SingleStoreResource
4
+ include Spree::Previewable
5
+
6
+ #
7
+ # Magic methods
8
+ #
9
+ acts_as_paranoid
10
+
11
+ #
12
+ # Validations
13
+ #
14
+ validates :name, :store, presence: true
15
+
16
+ #
17
+ # Associations
18
+ #
19
+ belongs_to :store, class_name: 'Spree::Store', touch: true
20
+ belongs_to :parent, class_name: 'Spree::Theme', optional: true, foreign_key: :parent_id
21
+ has_many :previews, class_name: 'Spree::Theme', foreign_key: :parent_id, dependent: :destroy_async
22
+ has_many :pages, -> { without_previews }, class_name: 'Spree::Page', dependent: :destroy, as: :pageable
23
+ has_many :page_previews, -> { only_previews }, class_name: 'Spree::Page', dependent: :destroy_async, as: :pageable
24
+ has_many :layout_sections, -> { order(position: :asc) }, class_name: 'Spree::PageSection', dependent: :destroy, as: :pageable
25
+ alias sections layout_sections
26
+
27
+ #
28
+ # Attachments
29
+ #
30
+ has_one_attached :screenshot, service: Spree.public_storage_service_name
31
+
32
+ #
33
+ #
34
+ # Callbacks
35
+ #
36
+ before_validation :set_name, on: :create
37
+ before_save :ensure_default_exists_and_is_unique
38
+ after_create :create_default_pages, :create_layout_sections, unless: :duplicating?
39
+ before_destroy :change_name_to_archived
40
+
41
+ #
42
+ # Virtual attributes
43
+ #
44
+ attribute :duplicating, :boolean, default: false
45
+
46
+ #
47
+ # Class methods
48
+ #
49
+ def self.to_param
50
+ Spree::Theme.to_s
51
+ end
52
+
53
+ # Returns an array of available themes, sorted by display name
54
+ #
55
+ # We need to load the theme classes to get the display name, so we also load the page classes at the same time
56
+ #
57
+ # @return [Array<Spree::Theme>]
58
+ def self.available_themes
59
+ @available_themes ||= Spree.page_builder.themes.sort_by(&:display_name)
60
+ end
61
+
62
+ def self.metadata
63
+ {
64
+ authors: [], # eg. ['Spree Commerce']
65
+ website: '', # eg. 'https://spreecommerce.org'
66
+ license: '', # eg. 'MIT', https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/licensing-a-repository#searching-github-by-license-type
67
+ description: '', # eg. 'A theme for Spree Commerce'
68
+ preview_image_url: '' # eg. 'https://example.com/screenshot.png'
69
+ }
70
+ end
71
+
72
+ def self.display_name
73
+ metadata[:name].presence || name.demodulize.titleize
74
+ end
75
+
76
+ def self.authors
77
+ metadata[:authors]
78
+ end
79
+
80
+ def self.license
81
+ metadata[:license]
82
+ end
83
+
84
+ def self.description
85
+ metadata[:description]
86
+ end
87
+
88
+ def self.preview_image_url
89
+ metadata[:preview_image_url]
90
+ end
91
+
92
+ def self.website
93
+ metadata[:website]
94
+ end
95
+
96
+ def duplicate
97
+ Themes::Duplicator.new(self).duplicate
98
+ end
99
+
100
+ def create_default_pages
101
+ Spree.page_builder.pages.map(&:to_s).map(&:constantize).each do |page_class|
102
+ next if page_class == Spree::Pages::Custom
103
+
104
+ page_class.where(pageable: self).first_or_create!
105
+ end
106
+ end
107
+
108
+ def create_layout_sections
109
+ ApplicationRecord.transaction do
110
+ available_layout_sections.map(&:to_s).map(&:constantize).each do |section_class|
111
+ section_class.where(pageable: self).first_or_create!
112
+ end
113
+ end
114
+ end
115
+
116
+ # Creates a new preview for the theme
117
+ #
118
+ # @return [Spree::Theme]
119
+ def create_preview
120
+ ActiveRecord::Base.connected_to(role: :writing) do
121
+ ApplicationRecord.transaction do
122
+ new_preview = dup
123
+ new_preview.parent = self
124
+ new_preview.duplicating = true
125
+ new_preview.default = false
126
+
127
+ # we need to deep clone layout sections and their assets
128
+ sections.includes(:links, { asset_attachment: :blob }, { blocks: [:rich_text_text, :links] }).each do |section|
129
+ section.deep_clone(new_preview)
130
+ end
131
+
132
+ new_preview.save!
133
+ new_preview
134
+ end
135
+ end
136
+ end
137
+
138
+ # Promotes the preview to the main theme
139
+ def promote
140
+ return unless preview?
141
+
142
+ ApplicationRecord.transaction do
143
+ old_theme = parent
144
+
145
+ # clear reference to the old theme and set default to the old theme's default
146
+ update!(parent: nil, default: old_theme.default)
147
+
148
+ # move pages to the new theme
149
+ old_theme.pages.update_all(pageable_id: id)
150
+
151
+ # destroy the old theme with their other previews, etc.
152
+ store.themes.find(old_theme.id).destroy
153
+
154
+ take_screenshot # update the screenshot
155
+ end
156
+ end
157
+
158
+ # Returns an array of available layout section classes for the theme, eg. header, footer, newsletter, etc.
159
+ #
160
+ # @return [Array<Class>]
161
+ def available_layout_sections
162
+ [
163
+ *Spree.page_builder.theme_layout_sections,
164
+ *custom_layout_sections
165
+ ]
166
+ end
167
+
168
+ # Returns an array of custom layout section classes for the theme
169
+ #
170
+ # @return [Array<Class>]
171
+ def custom_layout_sections
172
+ # you can override this method in your theme to return a list of custom layout sections for your theme
173
+ # [Spree::PageSections::Custom, Spree::PageSections::Custom2]
174
+ []
175
+ end
176
+
177
+ # Returns an array of available page section classes for the theme
178
+ #
179
+ # @return [Array<Class>]
180
+ def available_page_sections
181
+ return @available_page_sections if @available_page_sections
182
+
183
+ @available_page_sections ||= [
184
+ *Spree.page_builder.page_sections.find_all do |section_class|
185
+ section_class.role == 'content'
186
+ end,
187
+ *custom_page_sections
188
+ ].sort_by(&:name)
189
+ end
190
+
191
+ # Returns an array of custom page section classes for the theme
192
+ #
193
+ # @return [Array<Class>]
194
+ def custom_page_sections
195
+ # you can override this method in your theme to return a list of custom page sections for your theme
196
+ # [Spree::PageSections::Custom, Spree::PageSections::Custom2]
197
+ []
198
+ end
199
+
200
+ def restore_defaults!
201
+ self.preferences = {}
202
+ save!
203
+ end
204
+
205
+ def take_screenshot
206
+ return if Spree.screenshot_api_token.blank?
207
+ return if preview? # we don't want to take screenshots of previews, they aren't surfaced in the UI
208
+ return if screenshot.attached?
209
+
210
+ Spree::Themes::ScreenshotJob.perform_later(id)
211
+ end
212
+
213
+ protected
214
+
215
+ def set_name
216
+ self.name = type.demodulize.titleize if name.blank?
217
+ end
218
+
219
+ private
220
+
221
+ def ensure_default_exists_and_is_unique
222
+ if default
223
+ store.themes.where.not(id: id).update_all(default: false, updated_at: Time.current)
224
+ elsif store.themes.where(default: true).count.zero?
225
+ self.default = true
226
+ end
227
+ end
228
+
229
+ def change_name_to_archived
230
+ update_columns(name: "#{name} (Archived)")
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,97 @@
1
+ module Spree
2
+ module Themes
3
+ class Default < Spree::Theme
4
+ def self.metadata
5
+ {
6
+ authors: ['Spree Commerce'],
7
+ license: 'MIT',
8
+ preview_image_url: 'https://s3.eu-central-2.wasabisys.com/w.storage.screenshotapi.net/demo_spreecommerce_org_299a49137b25.png'
9
+ }
10
+ end
11
+
12
+ # COLORS
13
+ # main colors
14
+ preference :primary_color, :string, default: '#000000'
15
+ preference :accent_color, :string, default: '#F0EFE9'
16
+ preference :danger_color, :string, default: '#C73528'
17
+ preference :neutral_color, :string, default: '#999999'
18
+ preference :background_color, :string, default: '#FFFFFF'
19
+ preference :text_color, :string, default: '#000000'
20
+ preference :success_color, :string, default: '#00C773'
21
+
22
+ # buttons
23
+ preference :button_background_color, :string
24
+ preference :button_text_color, :string, default: '#ffffff'
25
+ preference :button_hover_background_color, :string
26
+ preference :button_hover_text_color, :string
27
+ preference :button_border_color, :string
28
+
29
+ # borders
30
+ preference :border_color, :string, default: '#E9E7DC'
31
+ preference :sidebar_border_color, :string
32
+
33
+ preference :secondary_button_background_color, :string
34
+ preference :secondary_button_text_color, :string
35
+ preference :secondary_button_hover_background_color, :string
36
+ preference :secondary_button_hover_text_color, :string
37
+
38
+ # inputs
39
+ preference :input_text_color, :string, default: '#6b7280'
40
+ preference :input_background_color, :string, default: '#ffffff'
41
+ preference :input_border_color, :string
42
+ preference :input_focus_border_color, :string
43
+ preference :input_focus_background_color, :string
44
+ preference :input_focus_text_color, :string
45
+
46
+ # sidebar (checkout)
47
+ preference :checkout_sidebar_background_color, :string, default: '#f3f4f6'
48
+ preference :checkout_divider_background_color, :string
49
+ preference :checkout_sidebar_text_color, :string
50
+
51
+ # TYPOGRAPHY
52
+ preference :custom_font_code, :string, default: nil
53
+ # body
54
+ preference :font_family, :string, default: 'Inter'
55
+ preference :font_size_scale, :integer, default: 100
56
+ # headers
57
+ preference :header_font_family, :string, default: 'Inter'
58
+ preference :header_font_size_scale, :integer, default: 100
59
+ preference :headings_uppercase, :boolean, default: true
60
+
61
+ # BUTTONS
62
+ preference :button_border_thickness, :integer, default: 1
63
+ preference :button_border_opacity, :integer, default: 100
64
+ preference :button_border_radius, :integer, default: 100
65
+ preference :button_shadow_opacity, :integer, default: 0
66
+ preference :button_shadow_horizontal_offset, :integer, default: 0
67
+ preference :button_shadow_vertical_offset, :integer, default: 4
68
+ preference :button_shadow_blur, :integer, default: 5
69
+
70
+ # INPUTS
71
+ preference :input_border_thickness, :integer, default: 1
72
+ preference :input_border_opacity, :integer, default: 100
73
+ preference :input_border_radius, :integer, default: 8
74
+ preference :input_shadow_opacity, :integer, default: 0
75
+ preference :input_shadow_horizontal_offset, :integer, default: 0
76
+ preference :input_shadow_vertical_offset, :integer, default: 4
77
+ preference :input_shadow_blur, :integer, default: 5
78
+
79
+ # BORDERS
80
+ preference :border_width, :integer, default: 1
81
+ preference :border_radius, :integer, default: 6
82
+ preference :border_shadow_opacity, :integer, default: 0
83
+ preference :border_shadow_horizontal_offset, :integer, default: 0
84
+ preference :border_shadow_vertical_offset, :integer, default: 4
85
+ preference :border_shadow_blur, :integer, default: 5
86
+
87
+ # PRODUCT IMAGES
88
+ # These defaults match preprocessed variant sizes for optimal performance:
89
+ # Desktop (360x360) → large variant (720x720 at 2x)
90
+ # Mobile (200x200) → medium variant (400x400 at 2x)
91
+ preference :product_listing_image_height, :integer, default: 360
92
+ preference :product_listing_image_width, :integer, default: 360
93
+ preference :product_listing_image_height_mobile, :integer, default: 200
94
+ preference :product_listing_image_width_mobile, :integer, default: 200
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,21 @@
1
+ module Spree
2
+ module Taxons
3
+ class TouchFeaturedSections
4
+ prepend Spree::ServiceModule::Base
5
+
6
+ def call(taxon_ids:)
7
+ return if taxon_ids.empty?
8
+
9
+ featured_taxons = Spree::PageSections::FeaturedTaxon.published.by_taxon_id(taxon_ids)
10
+
11
+ return if featured_taxons.empty?
12
+
13
+ featured_taxons.touch_all
14
+ pages = Spree::Page.where(id: featured_taxons.where(pageable_type: 'Spree::Page').pluck(:pageable_id))
15
+ pages.touch_all
16
+ themes = Spree::Theme.where(id: pages.where(pageable_type: 'Spree::Theme').pluck(:pageable_id).uniq)
17
+ themes.touch_all
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,46 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= I18n.locale %>">
3
+ <head>
4
+ <%= render 'spree/admin/shared/head' %>
5
+ <%= render "spree/admin/shared/custom_head" %>
6
+ </head>
7
+
8
+ <body class="admin page-builder"
9
+ id="page-builder"
10
+ data-controller="page-builder tabs dialog"
11
+ data-page-builder-preview-url-value="<%= page_preview_url %>"
12
+ >
13
+
14
+ <%= form_for @theme, as: :theme,url: spree.update_with_page_admin_theme_path(@theme), method: :put do |f| %>
15
+ <%= hidden_field_tag :page_id, @page.id %>
16
+ <%= hidden_field_tag :theme_preview_id, @theme_preview.id %>
17
+ <%= hidden_field_tag :page_preview_id, @page_preview.id %>
18
+ <%= render 'spree/admin/page_builder/header' %>
19
+ <% end %>
20
+
21
+ <main id="content">
22
+ <%= render 'spree/admin/page_builder/sidebar' %>
23
+ <main id="main-part">
24
+ <div class="desktopLiveView" data-page-builder-target="previewContainer">
25
+ <div class="embed-responsive embed-responsive-4by3">
26
+ <iframe class="embed-responsive-item overflow-y-auto"
27
+ data-page-builder-target="iframe"
28
+ data-action="load->page-builder#initializeVisualEditor"
29
+ id="page-builder-preview"
30
+ sandbox="allow-same-origin allow-forms allow-popups allow-scripts allow-modals">
31
+ </iframe>
32
+ <button class="editor-add-section btn btn-mint shadow" data-action="click->page-builder#addSection">
33
+ <%= icon("add", height: 22, class: "mr-2") %> Add Section
34
+ </button>
35
+ </div>
36
+ </div>
37
+
38
+ <%= turbo_frame_tag :iframe_preview_scripts %>
39
+ </main>
40
+ </main>
41
+
42
+ <%= render 'spree/admin/shared/dialog' %>
43
+ <%= render 'spree/admin/shared/alerts' %>
44
+ <%= render "spree/admin/shared/turbo_confirm_dialog" %>
45
+ </body>
46
+ </html>
@@ -0,0 +1,38 @@
1
+ <% if current_store.default_theme %>
2
+ <div class="card mb-6">
3
+ <%= link_to spree.edit_admin_theme_path(current_store.default_theme), class: 'block border rounded-lg m-2 overflow-auto bg-gray-25 relative store-preview-link hover:shadow-xs transition-shadow duration-200', data: { turbo_prefetch: false } do %> <div class="absolute w-full h-full items-center justify-center hover:opacity-100 opacity-0 transition-opacity duration-200 store-preview-actions">
4
+ <button class="btn btn-primary">
5
+ <%= icon('tools') %>
6
+ <%= Spree.t('admin.edit_theme') %>
7
+ </button>
8
+ </div>
9
+
10
+ <%= render 'spree/admin/themes/theme_preview_image', theme: current_store.default_theme, height: 172, width: 330 %>
11
+ <% end %>
12
+
13
+ <div class="card-body px-2 pt-0 pb-2 flex flex-col gap-2">
14
+ <div class="input-group pr-2">
15
+ <%= text_field_tag :domain, current_store.url_or_custom_domain, readonly: true, class: 'focus:outline-none focus:ring-0 text-base w-full' %>
16
+ <%= clipboard_component(current_store.url_or_custom_domain) %>
17
+ </div>
18
+
19
+ <div class="flex gap-2">
20
+ <%= link_to current_store.formatted_url_or_custom_domain, class: 'btn btn-light w-1/2', target: '_blank' do %>
21
+ <%= icon 'eye', class: 'mr-1' %>
22
+ &nbsp;
23
+ <%= Spree.t(:view_store) %>
24
+ <% end %>
25
+ <%= link_to spree.edit_admin_theme_path(current_store.default_theme), class: 'btn btn-light w-1/2', data: { turbo_prefetch: false } do %>
26
+ <%= icon 'tools', class: 'mr-1' %>
27
+ &nbsp;
28
+ <%= Spree.t('admin.edit_theme') %>
29
+ <% end %>
30
+ </div>
31
+ </div>
32
+ <% unless current_store.default_custom_domain&.active? %>
33
+ <div class="card-footer border-t p-2">
34
+ <%= link_to_with_icon 'world-www', 'Connect your own domain', spree.admin_custom_domains_path, class: 'btn btn-secondary w-full py-2' %>
35
+ </div>
36
+ <% end %>
37
+ </div>
38
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <% if @page_block.can_be_deleted? %>
2
+ <div class="sidebar-footer">
3
+ <%= link_to_delete(@page_block, url: spree.admin_page_section_block_path(@page_block.section, @page_block)) %>
4
+ </div>
5
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <%= turbo_stream.append "blocks_#{@page_block.section_id}" do %>
2
+ <%= render 'spree/admin/page_builder/sidebar_block', block: @page_block %>
3
+ <% end %>
4
+ <%= refresh_theme_preview %>
@@ -0,0 +1,9 @@
1
+ <%= turbo_stream.replace :page_sidebar do %>
2
+ <%= render 'spree/admin/page_builder/sidebar_sections' %>
3
+ <% end %>
4
+
5
+ <%= turbo_stream.replace 'page_sidebar_toolbar' do %>
6
+ <%= render 'spree/admin/page_builder/sidebar_sections_toolbar' %>
7
+ <% end %>
8
+
9
+ <%= refresh_theme_preview(@page_block.section) %>
@@ -0,0 +1,43 @@
1
+ <%= turbo_frame_tag :page_sidebar do %>
2
+ <% tab_selected ||= :content %>
3
+ <h6 class="sidebar-header">
4
+ <%= link_to spree.edit_admin_theme_path(@theme, page_id: @page&.id), data: { action: 'click->page-builder#clearActiveOverlays' }, class: 'btn hover:bg-gray-100 shadow-none px-3' do %>
5
+ <%= icon 'chevron-left', class: 'mr-0' %>
6
+ <% end %>
7
+ <%= @page_block.display_name %>
8
+ </h6>
9
+ <div class="p-3 edit-block">
10
+ <%= form_for @page_block, url: spree.admin_page_section_block_path(@page_block.section, @page_block), data: { controller: 'auto-submit' }, as: :page_block do |f| %>
11
+ <div data-controller="tabs">
12
+ <ul class="nav nav-pills nav-fill mb-6" id="pills-tab" role="tablist">
13
+ <li class="nav-item" role="presentation">
14
+ <button class="nav-link w-full <%= tab_selected == :content ? 'active' : '' %>" id="pills-home-tab" data-tabs-target="tab" data-action="click->tabs#select" type="button" role="tab" aria-controls="pills-home" aria-selected="<%= tab_selected == :content %>">Content</button>
15
+ </li>
16
+ <li class="nav-item" role="presentation">
17
+ <button class="nav-link w-full <%= tab_selected == :design ? 'active' : '' %>" id="pills-profile-tab" data-tabs-target="tab" data-action="click->tabs#select" type="button" role="tab" aria-controls="pills-profile" aria-selected="<%= tab_selected == :design %>">Design</button>
18
+ </li>
19
+ </ul>
20
+ <div data-tabs-target="panel" id="pills-home" role="tabpanel" aria-labelledby="pills-home-tab" class="animate-fade-in" <%= 'hidden' unless tab_selected == :content %>>
21
+ <%= render "spree/admin/page_blocks/forms/#{@page_block.form_partial_name}", f: f %>
22
+ <%= render 'spree/admin/page_blocks/form_tab_buttons', tab: :content %>
23
+ </div>
24
+ <div data-tabs-target="panel" id="pills-profile" role="tabpanel" aria-labelledby="pills-profile-tab" class="animate-fade-in" <%= 'hidden' unless tab_selected == :design %>>
25
+ <%= render 'spree/admin/page_sections/fields/top_padding', f: f %>
26
+ <%= render 'spree/admin/page_sections/fields/bottom_padding', f: f %>
27
+ <% if @page_block.respond_to?(:preferred_text_color) %>
28
+ <%= render 'spree/admin/page_sections/fields/text_color', f: f %>
29
+ <% end %>
30
+ <% if @page_block.respond_to?(:preferred_background_color) %>
31
+ <%= render 'spree/admin/page_sections/fields/background_color', f: f %>
32
+ <% end %>
33
+ <%= render 'spree/admin/page_sections/fields/button', f: f %>
34
+ <%= yield(:design_tab) %>
35
+
36
+ <div class="sidebar-footer">
37
+ <%= link_to_delete(@page_block, url: spree.admin_page_section_block_path(@page_block.section, @page_block)) %>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ <% end %>
42
+ </div>
43
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <% content_for(:design_tab) do %>
2
+ <div class="form-group">
3
+ <%= f.label :preferred_text_alignment, Spree.t(:text_alignment) %>
4
+ <%= f.select :preferred_text_alignment, options_for_select(['left', 'center', 'right'], @page_block.preferred_text_alignment), {}, { class: 'form-select', data: { action: 'auto-submit#submit' } } %>
5
+ </div>
6
+
7
+ <div class="form-group">
8
+ <%= f.label :preferred_container_alignment, Spree.t(:container_alignment) %>
9
+ <%= f.select :preferred_container_alignment, options_for_select(['left', 'center', 'right'], @page_block.preferred_container_alignment), {}, { class: 'form-select', data: { action: 'auto-submit#submit' } } %>
10
+ </div>
11
+
12
+ <%= render 'spree/admin/page_builder/labeled_range_input',
13
+ f: f, field: :preferred_width_desktop, min: 1, max: 100, unit: '%', _label: 'Max text width on desktop'
14
+ %>
15
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <div class="form-group">
2
+ <%= f.label :brand %>
3
+ <%= tom_select_tag 'page_block[brand_id]', active_option: @page_block.brand_id, class: 'w-full', url: spree.admin_brands_select_options_path(format: :json), select_data: {action: 'auto-submit#submit'} %>
4
+ </div>
@@ -0,0 +1,13 @@
1
+ <%= render 'spree/admin/page_blocks/forms/link', f: f %>
2
+
3
+ <div class="form-group">
4
+ <%= f.label :preferred_button_style_1, Spree.t('admin.page_builder.button_style') %>
5
+ <%= f.select :preferred_button_style_1, options_for_select(['primary', 'secondary'], @page_block.preferred_button_style_1), { }, { class: 'form-select', data: { action: 'auto-submit#submit' } } %>
6
+ </div>
7
+
8
+ <% content_for(:design_tab) do %>
9
+ <div class="form-group">
10
+ <%= f.label :preferred_text_alignment, Spree.t('admin.page_builder.button_alignment') %>
11
+ <%= f.select :preferred_text_alignment, options_for_select(['left', 'center', 'right'], @page_block.preferred_text_alignment), {}, { class: 'form-select', data: { action: 'auto-submit#submit' } } %>
12
+ </div>
13
+ <% end %>