panda-cms 0.7.4 → 0.8.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 (192) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +37 -2
  3. data/Rakefile +2 -0
  4. data/app/components/panda/cms/admin/statistics_component.rb +1 -2
  5. data/app/components/panda/cms/admin/user_activity_component.html.erb +3 -1
  6. data/app/components/panda/cms/admin/user_activity_component.rb +3 -5
  7. data/app/components/panda/cms/admin/user_display_component.html.erb +1 -1
  8. data/app/components/panda/cms/admin/user_display_component.rb +2 -2
  9. data/app/components/panda/cms/code_component.rb +8 -4
  10. data/app/components/panda/cms/menu_component.rb +7 -6
  11. data/app/components/panda/cms/page_menu_component.rb +15 -17
  12. data/app/components/panda/cms/rich_text_component.rb +10 -11
  13. data/app/components/panda/cms/text_component.rb +6 -7
  14. data/app/controllers/panda/cms/admin/base_controller.rb +18 -0
  15. data/app/controllers/panda/cms/admin/block_contents_controller.rb +1 -2
  16. data/app/controllers/panda/cms/admin/dashboard_controller.rb +14 -9
  17. data/app/controllers/panda/cms/admin/files_controller.rb +1 -3
  18. data/app/controllers/panda/cms/admin/forms_controller.rb +3 -6
  19. data/app/controllers/panda/cms/admin/menus_controller.rb +2 -3
  20. data/app/controllers/panda/cms/admin/pages_controller.rb +9 -8
  21. data/app/controllers/panda/cms/admin/posts_controller.rb +9 -11
  22. data/app/controllers/panda/cms/admin/settings/bulk_editor_controller.rb +32 -25
  23. data/app/controllers/panda/cms/admin/settings_controller.rb +13 -10
  24. data/app/controllers/panda/cms/application_controller.rb +19 -6
  25. data/app/controllers/panda/cms/errors_controller.rb +5 -2
  26. data/app/controllers/panda/cms/form_submissions_controller.rb +2 -0
  27. data/app/controllers/panda/cms/pages_controller.rb +34 -31
  28. data/app/controllers/panda/cms/posts_controller.rb +2 -0
  29. data/app/helpers/panda/cms/admin/files_helper.rb +5 -1
  30. data/app/helpers/panda/cms/admin/pages_helper.rb +5 -1
  31. data/app/helpers/panda/cms/application_helper.rb +3 -3
  32. data/app/helpers/panda/cms/asset_helper.rb +195 -0
  33. data/app/helpers/panda/cms/pages_helper.rb +2 -0
  34. data/app/helpers/panda/cms/posts_helper.rb +2 -0
  35. data/app/helpers/panda/cms/theme_helper.rb +2 -0
  36. data/app/javascript/panda/cms/application_panda_cms.js +2 -34
  37. data/app/javascript/panda/cms/controllers/editor_form_controller.js +59 -6
  38. data/app/javascript/panda/cms/controllers/index.js +8 -24
  39. data/app/javascript/panda/cms/stimulus-loading.js +39 -0
  40. data/app/javascript/panda_cms/stimulus-loading.js +39 -0
  41. data/app/jobs/panda/cms/application_job.rb +2 -0
  42. data/app/jobs/panda/cms/record_visit_job.rb +2 -0
  43. data/app/mailers/panda/cms/application_mailer.rb +2 -0
  44. data/app/mailers/panda/cms/form_mailer.rb +3 -1
  45. data/app/models/panda/cms/application_record.rb +2 -0
  46. data/app/models/panda/cms/block.rb +4 -1
  47. data/app/models/panda/cms/block_content.rb +3 -1
  48. data/app/models/panda/cms/current.rb +5 -12
  49. data/app/models/panda/cms/form.rb +2 -0
  50. data/app/models/panda/cms/form_submission.rb +2 -0
  51. data/app/models/panda/cms/menu.rb +12 -9
  52. data/app/models/panda/cms/menu_item.rb +10 -6
  53. data/app/models/panda/cms/page.rb +14 -12
  54. data/app/models/panda/cms/post.rb +12 -8
  55. data/app/models/panda/cms/redirect.rb +6 -3
  56. data/app/models/panda/cms/template.rb +12 -7
  57. data/app/models/panda/cms/visit.rb +3 -1
  58. data/app/models/panda/social/instagram_post.rb +2 -0
  59. data/app/services/panda/social/instagram_feed_service.rb +3 -1
  60. data/app/views/layouts/different_page.html.erb +6 -0
  61. data/app/views/layouts/homepage.html.erb +37 -0
  62. data/app/views/layouts/page.html.erb +18 -0
  63. data/app/views/layouts/panda/cms/application.html.erb +2 -1
  64. data/app/views/panda/cms/admin/dashboard/show.html.erb +2 -2
  65. data/app/views/panda/cms/admin/files/index.html.erb +1 -1
  66. data/app/views/panda/cms/admin/forms/index.html.erb +4 -4
  67. data/app/views/panda/cms/admin/forms/new.html.erb +2 -2
  68. data/app/views/panda/cms/admin/forms/show.html.erb +1 -1
  69. data/app/views/panda/cms/admin/menus/index.html.erb +4 -4
  70. data/app/views/panda/cms/admin/pages/edit.html.erb +6 -6
  71. data/app/views/panda/cms/admin/pages/index.html.erb +5 -5
  72. data/app/views/panda/cms/admin/pages/new.html.erb +16 -10
  73. data/app/views/panda/cms/admin/posts/_form.html.erb +1 -1
  74. data/app/views/panda/cms/admin/posts/edit.html.erb +2 -2
  75. data/app/views/panda/cms/admin/posts/index.html.erb +5 -5
  76. data/app/views/panda/cms/admin/posts/new.html.erb +1 -1
  77. data/app/views/panda/cms/admin/settings/bulk_editor/new.html.erb +1 -1
  78. data/app/views/panda/cms/admin/settings/index.html.erb +4 -4
  79. data/app/views/panda/cms/admin/shared/_breadcrumbs.html.erb +3 -3
  80. data/app/views/panda/cms/admin/shared/_flash.html.erb +1 -1
  81. data/app/views/panda/cms/admin/shared/_sidebar.html.erb +8 -8
  82. data/app/views/panda/cms/shared/_header.html.erb +10 -2
  83. data/app/views/panda/cms/shared/_importmap.html.erb +1 -1
  84. data/app/views/shared/_footer.html.erb +3 -0
  85. data/app/views/shared/_header.html.erb +11 -0
  86. data/config/importmap.rb +2 -0
  87. data/config/initializers/inflections.rb +2 -0
  88. data/config/initializers/panda/cms/form_errors.rb +20 -21
  89. data/config/initializers/panda/cms/healthcheck_log_silencer.rb +2 -0
  90. data/config/initializers/panda/cms.rb +8 -3
  91. data/config/initializers/zeitwork.rb +2 -0
  92. data/config/puma/test.rb +3 -1
  93. data/config/routes.rb +11 -19
  94. data/db/migrate/20240205223709_create_panda_cms_pages.rb +2 -0
  95. data/db/migrate/20240219213327_create_panda_cms_page_versions.rb +2 -0
  96. data/db/migrate/20240303002805_create_panda_cms_templates.rb +4 -1
  97. data/db/migrate/20240303003434_create_panda_cms_template_versions.rb +2 -0
  98. data/db/migrate/20240303022441_create_panda_cms_blocks.rb +4 -1
  99. data/db/migrate/20240303024256_create_panda_cms_block_contents.rb +2 -0
  100. data/db/migrate/20240303024746_create_panda_cms_block_content_versions.rb +2 -0
  101. data/db/migrate/20240303233238_add_panda_cms_menu_table.rb +2 -0
  102. data/db/migrate/20240303234724_add_panda_cms_menu_item_table.rb +2 -0
  103. data/db/migrate/20240304134343_add_parent_id_to_panda_cms_pages.rb +2 -0
  104. data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +7 -5
  105. data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +2 -0
  106. data/db/migrate/20240316212822_add_kind_to_panda_cms_menus.rb +3 -1
  107. data/db/migrate/20240316221425_add_start_page_to_panda_cms_menus.rb +2 -0
  108. data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +2 -0
  109. data/db/migrate/20240317010532_create_panda_cms_users.rb +2 -0
  110. data/db/migrate/20240317161534_add_max_uses_to_panda_cms_template.rb +2 -0
  111. data/db/migrate/20240317163053_reset_counter_cache_on_panda_cms_template.rb +2 -0
  112. data/db/migrate/20240317214827_create_panda_cms_redirects.rb +2 -0
  113. data/db/migrate/20240317230622_create_panda_cms_visits.rb +2 -0
  114. data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +5 -2
  115. data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +2 -0
  116. data/db/migrate/20240701225422_add_service_name_to_active_storage_blobs.active_storage.rb +8 -6
  117. data/db/migrate/20240701225423_create_active_storage_variant_records.active_storage.rb +2 -0
  118. data/db/migrate/20240701225424_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +2 -0
  119. data/db/migrate/20240804235210_create_panda_cms_forms.rb +2 -0
  120. data/db/migrate/20240805013612_create_panda_cms_form_submissions.rb +2 -0
  121. data/db/migrate/20240805121123_create_panda_cms_posts.rb +3 -1
  122. data/db/migrate/20240805123104_create_panda_cms_post_versions.rb +2 -0
  123. data/db/migrate/20240806112735_fix_panda_cms_visits_column_names.rb +2 -0
  124. data/db/migrate/20240806204412_add_completion_path_to_panda_cms_forms.rb +2 -0
  125. data/db/migrate/20240820081917_change_form_submissions_to_submission_count.rb +2 -0
  126. data/db/migrate/20240923234535_add_depth_to_panda_cms_menus.rb +6 -4
  127. data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +2 -0
  128. data/db/migrate/20241119214548_convert_post_content_to_editor_js.rb +2 -0
  129. data/db/migrate/20241120000419_remove_post_tag_references.rb +2 -0
  130. data/db/migrate/20241120110943_add_editor_js_to_posts.rb +2 -0
  131. data/db/migrate/20241120113859_add_cached_content_to_panda_cms_posts.rb +2 -0
  132. data/db/migrate/20241123234140_remove_post_tag_id_from_posts.rb +2 -0
  133. data/db/migrate/20250106223303_add_author_id_to_panda_cms_posts.rb +5 -1
  134. data/db/migrate/20250120235542_remove_paper_trail.rb +5 -4
  135. data/db/migrate/20250126234001_create_panda_social_instagram_posts.rb +4 -0
  136. data/db/migrate/20250809231125_migrate_users_to_panda_core.rb +111 -0
  137. data/db/migrate/20250811111000_make_post_user_references_nullable.rb +11 -0
  138. data/db/seeds.rb +2 -0
  139. data/lib/generators/panda/cms/install_generator.rb +2 -0
  140. data/lib/panda/cms/asset_loader.rb +390 -0
  141. data/lib/panda/cms/bulk_editor.rb +7 -3
  142. data/lib/panda/cms/demo_site_generator.rb +2 -0
  143. data/lib/panda/cms/engine.rb +57 -116
  144. data/lib/panda/cms/exceptions_app.rb +2 -0
  145. data/lib/panda/cms/railtie.rb +2 -0
  146. data/lib/panda/cms/slug.rb +3 -1
  147. data/lib/panda-cms/version.rb +3 -1
  148. data/lib/panda-cms.rb +54 -42
  149. data/lib/tasks/assets.rake +587 -0
  150. data/lib/tasks/panda/cms/install.rake +2 -0
  151. data/lib/tasks/panda/cms/migrations.rake +13 -0
  152. data/lib/tasks/panda/social/instagram.rake +2 -0
  153. data/lib/tasks/panda_cms.rake +3 -30
  154. data/public/panda-cms-assets/manifest.json +20 -0
  155. data/public/panda-cms-assets/panda-cms-0.7.4.css +26 -0
  156. data/public/panda-cms-assets/panda-cms-0.7.4.js +150 -0
  157. metadata +186 -49
  158. data/app/builders/panda/cms/form_builder.rb +0 -217
  159. data/app/components/panda/cms/admin/button_component.rb +0 -70
  160. data/app/components/panda/cms/admin/container_component.rb +0 -13
  161. data/app/components/panda/cms/admin/flash_message_component.rb +0 -47
  162. data/app/components/panda/cms/admin/heading_component.rb +0 -45
  163. data/app/components/panda/cms/admin/panel_component.rb +0 -13
  164. data/app/components/panda/cms/admin/table_component.rb +0 -46
  165. data/app/components/panda/cms/admin/tag_component.rb +0 -35
  166. data/app/constraints/panda/cms/admin_constraint.rb +0 -18
  167. data/app/controllers/panda/cms/admin/my_profile_controller.rb +0 -43
  168. data/app/controllers/panda/cms/admin/sessions_controller.rb +0 -94
  169. data/app/javascript/panda/cms/controllers/theme_form_controller.js +0 -9
  170. data/app/javascript/panda/cms/editor/css_extractor.js +0 -80
  171. data/app/javascript/panda/cms/editor/editor_js_config.js +0 -306
  172. data/app/javascript/panda/cms/editor/editor_js_initializer.js +0 -334
  173. data/app/javascript/panda/cms/editor/plain_text_editor.js +0 -110
  174. data/app/javascript/panda/cms/editor/resource_loader.js +0 -204
  175. data/app/javascript/panda/cms/editor/rich_text_editor.js +0 -162
  176. data/app/models/panda/cms/breadcrumb.rb +0 -12
  177. data/app/models/panda/cms/user.rb +0 -31
  178. data/app/services/panda/cms/html_to_editor_js_converter.rb +0 -193
  179. data/app/views/panda/cms/admin/my_profile/edit.html.erb +0 -35
  180. data/app/views/panda/cms/admin/sessions/new.html.erb +0 -17
  181. data/db/migrate/20250504221812_add_current_theme_to_panda_cms_users.rb +0 -5
  182. data/lib/panda/cms/editor_js/blocks/alert.rb +0 -34
  183. data/lib/panda/cms/editor_js/blocks/base.rb +0 -33
  184. data/lib/panda/cms/editor_js/blocks/header.rb +0 -15
  185. data/lib/panda/cms/editor_js/blocks/image.rb +0 -36
  186. data/lib/panda/cms/editor_js/blocks/list.rb +0 -32
  187. data/lib/panda/cms/editor_js/blocks/paragraph.rb +0 -15
  188. data/lib/panda/cms/editor_js/blocks/quote.rb +0 -41
  189. data/lib/panda/cms/editor_js/blocks/table.rb +0 -50
  190. data/lib/panda/cms/editor_js/renderer.rb +0 -124
  191. data/lib/panda/cms/editor_js.rb +0 -16
  192. data/lib/panda/cms/editor_js_content.rb +0 -55
@@ -1,162 +0,0 @@
1
- import EditorJS from "@editorjs/editorjs"
2
- import Paragraph from "@editorjs/paragraph"
3
- import Header from "@editorjs/header"
4
- import List from "@editorjs/list"
5
- import Quote from "@editorjs/quote"
6
- import Table from "@editorjs/table"
7
- import NestedList from "@editorjs/nested-list"
8
-
9
- export default class RichTextEditor {
10
- constructor(element, iframe) {
11
- this.element = element
12
- this.iframe = iframe
13
- this.editor = null
14
- this.initialized = false
15
- this.initialize()
16
- }
17
-
18
- async initialize() {
19
- if (this.initialized) return
20
- console.debug("[Panda CMS] Initializing EditorJS")
21
-
22
- try {
23
- let content = this.element.dataset.editableContent || ""
24
- let previousData = this.element.dataset.editablePreviousData || ""
25
- console.debug("[Panda CMS] Initial content:", content)
26
- console.debug("[Panda CMS] Previous data:", previousData)
27
-
28
- let parsedContent
29
- if (previousData) {
30
- try {
31
- // Try to decode base64 first
32
- const decodedData = atob(previousData)
33
- console.debug("[Panda CMS] Decoded base64 data:", decodedData)
34
- parsedContent = JSON.parse(decodedData)
35
- console.debug("[Panda CMS] Successfully parsed base64 data:", parsedContent)
36
- } catch (e) {
37
- console.debug("[Panda CMS] Not base64 encoded or invalid, trying direct JSON parse:", e)
38
- try {
39
- parsedContent = JSON.parse(previousData)
40
- console.debug("[Panda CMS] Successfully parsed JSON data:", parsedContent)
41
- } catch (e2) {
42
- console.error("[Panda CMS] Failed to parse previous data:", e2)
43
- parsedContent = this.getDefaultContent()
44
- }
45
- }
46
- } else if (content) {
47
- try {
48
- parsedContent = JSON.parse(content)
49
- console.debug("[Panda CMS] Successfully parsed content:", parsedContent)
50
- } catch (e) {
51
- console.error("[Panda CMS] Failed to parse content:", e)
52
- parsedContent = this.getDefaultContent()
53
- }
54
- } else {
55
- parsedContent = this.getDefaultContent()
56
- }
57
-
58
- // Create holder element before initialization
59
- const holderId = `editor-${Math.random().toString(36).substr(2, 9)}`
60
- const holderElement = document.createElement("div")
61
- holderElement.id = holderId
62
- holderElement.className = "editor-js-holder codex-editor"
63
-
64
- // Clear any existing content and append holder
65
- this.element.textContent = ""
66
- this.element.appendChild(holderElement)
67
-
68
- // Initialize EditorJS
69
- this.editor = new EditorJS({
70
- holder: holderId,
71
- data: parsedContent,
72
- placeholder: "Click to start writing...",
73
- tools: {
74
- paragraph: {
75
- class: Paragraph,
76
- inlineToolbar: true
77
- },
78
- header: {
79
- class: Header,
80
- inlineToolbar: true
81
- },
82
- list: {
83
- class: NestedList,
84
- inlineToolbar: true,
85
- config: {
86
- defaultStyle: 'unordered',
87
- enableLineBreaks: true
88
- }
89
- },
90
- quote: {
91
- class: Quote,
92
- inlineToolbar: true
93
- },
94
- table: {
95
- class: Table,
96
- inlineToolbar: true
97
- }
98
- },
99
- onChange: () => {
100
- this.save()
101
- }
102
- })
103
-
104
- await this.editor.isReady
105
- this.initialized = true
106
- console.debug("[Panda CMS] EditorJS initialized successfully")
107
- } catch (error) {
108
- console.error("[Panda CMS] Error initializing EditorJS:", error)
109
- }
110
- }
111
-
112
- getDefaultContent() {
113
- return {
114
- time: Date.now(),
115
- blocks: [
116
- {
117
- type: "paragraph",
118
- data: {
119
- text: ""
120
- }
121
- }
122
- ],
123
- version: "2.28.2"
124
- }
125
- }
126
-
127
- async save() {
128
- if (!this.editor) return null
129
-
130
- try {
131
- const savedData = await this.editor.save()
132
- const jsonString = JSON.stringify(savedData)
133
- // Store both base64 and regular JSON
134
- this.element.dataset.editablePreviousData = btoa(jsonString)
135
- this.element.dataset.editableContent = jsonString
136
- return jsonString
137
- } catch (error) {
138
- console.error("[Panda CMS] Error saving EditorJS content:", error)
139
- return null
140
- }
141
- }
142
-
143
- async clear() {
144
- if (!this.editor) return
145
-
146
- try {
147
- await this.editor.clear()
148
- this.element.dataset.editablePreviousData = ""
149
- this.element.dataset.editableContent = ""
150
- } catch (error) {
151
- console.error("[Panda CMS] Error clearing EditorJS content:", error)
152
- }
153
- }
154
-
155
- destroy() {
156
- if (this.editor) {
157
- this.editor.destroy()
158
- this.editor = null
159
- this.initialized = false
160
- }
161
- }
162
- }
@@ -1,12 +0,0 @@
1
- module Panda
2
- module CMS
3
- class Breadcrumb
4
- attr_reader :name, :path
5
-
6
- def initialize(name, path)
7
- @name = name
8
- @path = path
9
- end
10
- end
11
- end
12
- end
@@ -1,31 +0,0 @@
1
- module Panda
2
- module CMS
3
- class User < ApplicationRecord
4
- validates :firstname, presence: true
5
- validates :lastname, presence: true
6
- validates :email, presence: true, uniqueness: true
7
-
8
- before_save :downcase_email
9
-
10
- scope :admin, -> { where(admin: true) }
11
-
12
- def is_admin?
13
- admin
14
- end
15
-
16
- def name
17
- "#{firstname} #{lastname}"
18
- end
19
-
20
- def self.for_select_list(scope = :all, order = {firstname: :asc, lastname: :asc})
21
- Panda::CMS::User.send(scope).order(order).map { |u| [u.name, u.id] }
22
- end
23
-
24
- private
25
-
26
- def downcase_email
27
- self.email = email.to_s.downcase
28
- end
29
- end
30
- end
31
- end
@@ -1,193 +0,0 @@
1
- module Panda
2
- module CMS
3
- class HtmlToEditorJsConverter
4
- class ConversionError < StandardError; end
5
-
6
- def self.convert(html)
7
- return {} if html.blank?
8
-
9
- # If it's already in EditorJS format, return as is
10
- return html if html.is_a?(Hash) && (html["blocks"].present? || html[:blocks].present?)
11
-
12
- begin
13
- # Parse the HTML content
14
- doc = Nokogiri::HTML.fragment(html.to_s)
15
- raise ConversionError, "Failed to parse HTML content" unless doc
16
-
17
- blocks = []
18
- current_text = ""
19
-
20
- doc.children.each do |node|
21
- case node.name
22
- when "h1", "h2", "h3", "h4", "h5", "h6"
23
- # Add any accumulated text as a paragraph before the header
24
- if current_text.present?
25
- blocks << create_paragraph_block(current_text)
26
- current_text = ""
27
- end
28
-
29
- blocks << {
30
- "type" => "header",
31
- "data" => {
32
- "text" => node.text.strip,
33
- "level" => node.name[1].to_i
34
- }
35
- }
36
- when "p", "div"
37
- # Add any accumulated text first
38
- if current_text.present?
39
- blocks << create_paragraph_block(current_text)
40
- current_text = ""
41
- end
42
-
43
- if node.name == "div"
44
- # Process div children separately
45
- node.children.each do |child|
46
- case child.name
47
- when "h1", "h2", "h3", "h4", "h5", "h6"
48
- blocks << {
49
- "type" => "header",
50
- "data" => {
51
- "text" => child.text.strip,
52
- "level" => child.name[1].to_i
53
- }
54
- }
55
- when "p"
56
- text = process_inline_elements(child)
57
- paragraphs = text.split(/<br\s*\/?>\s*<br\s*\/?>/).map(&:strip)
58
- paragraphs.each do |paragraph|
59
- blocks << create_paragraph_block(paragraph) if paragraph.present?
60
- end
61
- when "ul", "ol"
62
- items = child.css("li").map { |li| process_inline_elements(li) }
63
- next if items.empty?
64
-
65
- blocks << {
66
- "type" => "list",
67
- "data" => {
68
- "style" => (child.name == "ul") ? "unordered" : "ordered",
69
- "items" => items
70
- }
71
- }
72
- when "blockquote"
73
- blocks << {
74
- "type" => "quote",
75
- "data" => {
76
- "text" => process_inline_elements(child),
77
- "caption" => "",
78
- "alignment" => "left"
79
- }
80
- }
81
- when "text"
82
- text = child.text.strip
83
- current_text += text if text.present?
84
- end
85
- end
86
- else
87
- # Handle p with nested content
88
- text = process_inline_elements(node)
89
- paragraphs = text.split(/<br\s*\/?>\s*<br\s*\/?>/).map(&:strip)
90
- paragraphs.each do |paragraph|
91
- blocks << create_paragraph_block(paragraph) if paragraph.present?
92
- end
93
- end
94
- when "br"
95
- current_text += "\n\n"
96
- when "text"
97
- text = node.text.strip
98
- current_text += text if text.present?
99
- when "ul", "ol"
100
- # Add any accumulated text first
101
- if current_text.present?
102
- blocks << create_paragraph_block(current_text)
103
- current_text = ""
104
- end
105
-
106
- items = node.css("li").map { |li| process_inline_elements(li) }
107
- next if items.empty?
108
-
109
- blocks << {
110
- "type" => "list",
111
- "data" => {
112
- "style" => (node.name == "ul") ? "unordered" : "ordered",
113
- "items" => items
114
- }
115
- }
116
- when "blockquote"
117
- # Add any accumulated text first
118
- if current_text.present?
119
- blocks << create_paragraph_block(current_text)
120
- current_text = ""
121
- end
122
-
123
- blocks << {
124
- "type" => "quote",
125
- "data" => {
126
- "text" => process_inline_elements(node),
127
- "caption" => "",
128
- "alignment" => "left"
129
- }
130
- }
131
- end
132
- end
133
-
134
- # Add any remaining text
135
- blocks << create_paragraph_block(current_text) if current_text.present?
136
-
137
- # Return the complete EditorJS structure
138
- {
139
- "time" => Time.current.to_i * 1000,
140
- "blocks" => blocks,
141
- "version" => "2.28.2"
142
- }
143
- rescue => e
144
- Rails.logger.error "HTML to EditorJS conversion failed: #{e.message}"
145
- Rails.logger.error e.backtrace.join("\n")
146
- raise ConversionError, "Failed to convert HTML to EditorJS format: #{e.message}"
147
- end
148
- end
149
-
150
- def self.create_paragraph_block(text)
151
- {
152
- "type" => "paragraph",
153
- "data" => {
154
- "text" => text.strip
155
- }
156
- }
157
- end
158
-
159
- def self.process_inline_elements(node)
160
- result = ""
161
- node.children.each do |child|
162
- case child.name
163
- when "br"
164
- result += "<br>"
165
- when "text"
166
- result += child.text
167
- when "strong", "b"
168
- result += "<b>#{child.text}</b>"
169
- when "em", "i"
170
- result += "<i>#{child.text}</i>"
171
- when "a"
172
- href = child["href"]
173
- text = child.text.strip
174
- # Handle email links specially
175
- if href&.start_with?("mailto:")
176
- email = href.sub("mailto:", "")
177
- result += "<a href=\"mailto:#{email}\">#{text}</a>"
178
- else
179
- result += "<a href=\"#{href}\">#{text}</a>"
180
- end
181
- else
182
- result += if child.text?
183
- child.text
184
- else
185
- child.to_html
186
- end
187
- end
188
- end
189
- result.strip
190
- end
191
- end
192
- end
193
- end
@@ -1,35 +0,0 @@
1
- <%= render Panda::CMS::Admin::ContainerComponent.new do |component| %>
2
- <% component.with_heading(text: "My Profile", level: 1) %>
3
-
4
- <%= panda_cms_form_with model: user,
5
- url: admin_my_profile_path,
6
- method: :patch,
7
- data: { controller: "theme-form" } do |f| %>
8
- <% if user.errors.any? %>
9
- <div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-md">
10
- <div class="text-sm text-red-600">
11
- <% user.errors.full_messages.each do |message| %>
12
- <p><%= message %></p>
13
- <% end %>
14
- </div>
15
- </div>
16
- <% end %>
17
-
18
- <div class="space-y-4">
19
- <%= f.text_field :firstname %>
20
-
21
- <%= f.text_field :lastname %>
22
-
23
- <%= f.email_field :email %>
24
-
25
- <%= f.select :current_theme,
26
- [["Default", "default"], ["Sky", "sky"]],
27
- { label: "Theme" },
28
- data: { action: "change->theme-form#updateTheme" } %>
29
- </div>
30
-
31
- <%= f.submit "Update Profile",
32
- class: "btn btn-primary mt-6",
33
- data: { disable_with: "Saving..." } %>
34
- <% end %>
35
- <% end %>
@@ -1,17 +0,0 @@
1
- <div class="flex flex-col justify-center py-12 px-6 min-h-full text-center lg:px-8">
2
- <div class="text-center sm:mx-auto sm:w-full sm:max-w-sm">
3
- <img src="/panda-cms-assets/panda-nav.png" class="py-2 mx-auto w-auto h-32">
4
- <h2 class="mt-10 mb-6 text-2xl font-bold text-center text-white"><%= t("panda.cms.admin.sessions.new.title") %></h2>
5
- </div>
6
- <% @providers.each do |provider| %>
7
- <div class="mt-4 text-center sm:mx-auto sm:w-full sm:max-w-sm">
8
- <%= form_tag "#{Panda::CMS.root_path}/auth/#{provider}", method: "post", data: {turbo: false} do %>
9
- <input type="hidden" name="redirect_uri" value="<%= admin_login_callback_url(provider: provider) %>">
10
- <button type="submit" id="button-sign-in-<%= provider %>" class="inline-flex gap-x-2 items-center py-2.5 px-3.5 mx-auto mb-4 bg-white rounded-md border min-w-56 border-neutral-400">
11
- <i class="fa-brands fa-<%= provider %> text-xl mr-1"></i>
12
- <%= t("panda.cms.authentication.sign_in_with", provider: t("panda.cms.authentication.providers.#{provider}")) %>
13
- </button>
14
- <% end %>
15
- </div>
16
- <% end %>
17
- </div>
@@ -1,5 +0,0 @@
1
- class AddCurrentThemeToPandaCMSUsers < ActiveRecord::Migration[8.0]
2
- def change
3
- add_column :panda_cms_users, :current_theme, :string, default: "default"
4
- end
5
- end
@@ -1,34 +0,0 @@
1
- module Panda
2
- module CMS
3
- module EditorJs
4
- module Blocks
5
- class Alert < Base
6
- def render
7
- message = sanitize(data["message"])
8
- type = data["type"] || "primary"
9
-
10
- html_safe(
11
- "<div class=\"#{alert_classes(type)} p-4 mb-4 rounded-lg\">" \
12
- "#{message}" \
13
- "</div>"
14
- )
15
- end
16
-
17
- private
18
-
19
- def alert_classes(type)
20
- case type
21
- when "primary" then "bg-blue-100 text-blue-800"
22
- when "secondary" then "bg-gray-100 text-gray-800"
23
- when "success" then "bg-green-100 text-green-800"
24
- when "danger" then "bg-red-100 text-red-800"
25
- when "warning" then "bg-yellow-100 text-yellow-800"
26
- when "info" then "bg-indigo-100 text-indigo-800"
27
- else "bg-blue-100 text-blue-800"
28
- end
29
- end
30
- end
31
- end
32
- end
33
- end
34
- end
@@ -1,33 +0,0 @@
1
- module Panda
2
- module CMS
3
- module EditorJs
4
- module Blocks
5
- class Base
6
- include ActionView::Helpers::SanitizeHelper
7
- include ActionView::Helpers::TagHelper
8
-
9
- attr_reader :data, :options
10
-
11
- def initialize(data, options = {})
12
- @data = data
13
- @options = options
14
- end
15
-
16
- def render
17
- ""
18
- end
19
-
20
- protected
21
-
22
- def html_safe(content)
23
- content.html_safe
24
- end
25
-
26
- def sanitize(text)
27
- Rails::Html::SafeListSanitizer.new.sanitize(text, tags: %w[b i u a code])
28
- end
29
- end
30
- end
31
- end
32
- end
33
- end
@@ -1,15 +0,0 @@
1
- module Panda
2
- module CMS
3
- module EditorJs
4
- module Blocks
5
- class Header < Base
6
- def render
7
- content = sanitize(data["text"])
8
- level = data["level"] || 2
9
- html_safe("<h#{level}>#{content}</h#{level}>")
10
- end
11
- end
12
- end
13
- end
14
- end
15
- end
@@ -1,36 +0,0 @@
1
- module Panda
2
- module CMS
3
- module EditorJs
4
- module Blocks
5
- class Image < Base
6
- def render
7
- url = data["url"]
8
- caption = sanitize(data["caption"])
9
- with_border = data["withBorder"]
10
- with_background = data["withBackground"]
11
- stretched = data["stretched"]
12
-
13
- css_classes = ["prose"]
14
- css_classes << "border" if with_border
15
- css_classes << "bg-gray-100" if with_background
16
- css_classes << "w-full" if stretched
17
-
18
- html_safe(<<~HTML)
19
- <figure class="#{css_classes.join(" ")}">
20
- <img src="#{url}" alt="#{caption}" />
21
- #{caption_element(caption)}
22
- </figure>
23
- HTML
24
- end
25
-
26
- private
27
-
28
- def caption_element(caption)
29
- return "" if caption.blank?
30
- "<figcaption>#{caption}</figcaption>"
31
- end
32
- end
33
- end
34
- end
35
- end
36
- end
@@ -1,32 +0,0 @@
1
- module Panda
2
- module CMS
3
- module EditorJs
4
- module Blocks
5
- class List < Base
6
- def render
7
- list_type = (data["style"] == "ordered") ? "ol" : "ul"
8
- html_safe(
9
- "<#{list_type}>" \
10
- "#{render_items(data["items"])}" \
11
- "</#{list_type}>"
12
- )
13
- end
14
-
15
- private
16
-
17
- def render_items(items)
18
- items.map do |item|
19
- content = item.is_a?(Hash) ? item["content"] : item
20
- nested = (item.is_a?(Hash) && item["items"].present?) ? render_nested(item["items"]) : ""
21
- "<li>#{sanitize(content)}#{nested}</li>"
22
- end.join
23
- end
24
-
25
- def render_nested(items)
26
- self.class.new({"items" => items, "style" => data["style"]}).render
27
- end
28
- end
29
- end
30
- end
31
- end
32
- end
@@ -1,15 +0,0 @@
1
- module Panda
2
- module CMS
3
- module EditorJs
4
- module Blocks
5
- class Paragraph < Base
6
- def render
7
- content = sanitize(data["text"])
8
- return "" if content.blank?
9
- html_safe("<p>#{content}</p>")
10
- end
11
- end
12
- end
13
- end
14
- end
15
- end
@@ -1,41 +0,0 @@
1
- module Panda
2
- module CMS
3
- module EditorJs
4
- module Blocks
5
- class Quote < Base
6
- def render
7
- text = data["text"]
8
- caption = data["caption"]
9
- alignment = data["alignment"] || "left"
10
-
11
- # Build the HTML structure
12
- html = "<figure class=\"text-#{alignment}\">" \
13
- "<blockquote>#{wrap_text_in_p(text)}</blockquote>" \
14
- "#{caption_element(caption)}" \
15
- "</figure>"
16
-
17
- # Return raw HTML - validation will be handled by the main renderer if enabled
18
- html_safe(html)
19
- end
20
-
21
- private
22
-
23
- def wrap_text_in_p(text)
24
- # Only wrap in <p> if it's not already wrapped
25
- text = sanitize(text)
26
- if text.start_with?("<p>") && text.end_with?("</p>")
27
- text
28
- else
29
- "<p>#{text}</p>"
30
- end
31
- end
32
-
33
- def caption_element(caption)
34
- return "" if caption.blank?
35
- "<figcaption>#{sanitize(caption)}</figcaption>"
36
- end
37
- end
38
- end
39
- end
40
- end
41
- end