panda-cms 0.7.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 (233) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +73 -0
  3. data/Rakefile +7 -0
  4. data/app/assets/builds/panda.cms.css +2808 -0
  5. data/app/assets/config/panda_cms_manifest.js +4 -0
  6. data/app/assets/stylesheets/panda/cms/application.tailwind.css +162 -0
  7. data/app/assets/stylesheets/panda/cms/editor.css +120 -0
  8. data/app/builders/panda/cms/form_builder.rb +234 -0
  9. data/app/components/panda/cms/admin/button_component.rb +70 -0
  10. data/app/components/panda/cms/admin/container_component.html.erb +13 -0
  11. data/app/components/panda/cms/admin/container_component.rb +13 -0
  12. data/app/components/panda/cms/admin/flash_message_component.html.erb +31 -0
  13. data/app/components/panda/cms/admin/flash_message_component.rb +47 -0
  14. data/app/components/panda/cms/admin/heading_component.rb +45 -0
  15. data/app/components/panda/cms/admin/panel_component.html.erb +7 -0
  16. data/app/components/panda/cms/admin/panel_component.rb +13 -0
  17. data/app/components/panda/cms/admin/slideover_component.html.erb +9 -0
  18. data/app/components/panda/cms/admin/slideover_component.rb +15 -0
  19. data/app/components/panda/cms/admin/statistics_component.html.erb +4 -0
  20. data/app/components/panda/cms/admin/statistics_component.rb +17 -0
  21. data/app/components/panda/cms/admin/tab_bar_component.html.erb +35 -0
  22. data/app/components/panda/cms/admin/tab_bar_component.rb +15 -0
  23. data/app/components/panda/cms/admin/table_component.html.erb +29 -0
  24. data/app/components/panda/cms/admin/table_component.rb +46 -0
  25. data/app/components/panda/cms/admin/tag_component.rb +35 -0
  26. data/app/components/panda/cms/admin/user_activity_component.html.erb +5 -0
  27. data/app/components/panda/cms/admin/user_activity_component.rb +33 -0
  28. data/app/components/panda/cms/admin/user_display_component.html.erb +17 -0
  29. data/app/components/panda/cms/admin/user_display_component.rb +21 -0
  30. data/app/components/panda/cms/code_component.rb +64 -0
  31. data/app/components/panda/cms/grid_component.html.erb +6 -0
  32. data/app/components/panda/cms/grid_component.rb +15 -0
  33. data/app/components/panda/cms/menu_component.html.erb +6 -0
  34. data/app/components/panda/cms/menu_component.rb +58 -0
  35. data/app/components/panda/cms/page_menu_component.html.erb +21 -0
  36. data/app/components/panda/cms/page_menu_component.rb +38 -0
  37. data/app/components/panda/cms/rich_text_component.html.erb +6 -0
  38. data/app/components/panda/cms/rich_text_component.rb +84 -0
  39. data/app/components/panda/cms/text_component.rb +72 -0
  40. data/app/constraints/panda/cms/admin_constraint.rb +18 -0
  41. data/app/controllers/panda/cms/admin/block_contents_controller.rb +52 -0
  42. data/app/controllers/panda/cms/admin/dashboard_controller.rb +20 -0
  43. data/app/controllers/panda/cms/admin/files_controller.rb +21 -0
  44. data/app/controllers/panda/cms/admin/forms_controller.rb +53 -0
  45. data/app/controllers/panda/cms/admin/menus_controller.rb +30 -0
  46. data/app/controllers/panda/cms/admin/pages_controller.rb +91 -0
  47. data/app/controllers/panda/cms/admin/posts_controller.rb +146 -0
  48. data/app/controllers/panda/cms/admin/sessions_controller.rb +94 -0
  49. data/app/controllers/panda/cms/admin/settings/bulk_editor_controller.rb +37 -0
  50. data/app/controllers/panda/cms/admin/settings_controller.rb +20 -0
  51. data/app/controllers/panda/cms/application_controller.rb +57 -0
  52. data/app/controllers/panda/cms/errors_controller.rb +33 -0
  53. data/app/controllers/panda/cms/form_submissions_controller.rb +23 -0
  54. data/app/controllers/panda/cms/pages_controller.rb +72 -0
  55. data/app/controllers/panda/cms/posts_controller.rb +13 -0
  56. data/app/helpers/panda/cms/admin/files_helper.rb +6 -0
  57. data/app/helpers/panda/cms/admin/pages_helper.rb +6 -0
  58. data/app/helpers/panda/cms/admin/posts_helper.rb +48 -0
  59. data/app/helpers/panda/cms/application_helper.rb +120 -0
  60. data/app/helpers/panda/cms/pages_helper.rb +6 -0
  61. data/app/helpers/panda/cms/theme_helper.rb +18 -0
  62. data/app/javascript/panda/cms/@editorjs--editorjs.js +2577 -0
  63. data/app/javascript/panda/cms/@hotwired--stimulus.js +4 -0
  64. data/app/javascript/panda/cms/@hotwired--turbo.js +160 -0
  65. data/app/javascript/panda/cms/@rails--actioncable--src.js +4 -0
  66. data/app/javascript/panda/cms/application_panda_cms.js +39 -0
  67. data/app/javascript/panda/cms/controllers/dashboard_controller.js +7 -0
  68. data/app/javascript/panda/cms/controllers/editor_form_controller.js +77 -0
  69. data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +320 -0
  70. data/app/javascript/panda/cms/controllers/index.js +48 -0
  71. data/app/javascript/panda/cms/controllers/slug_controller.js +87 -0
  72. data/app/javascript/panda/cms/editor/css_extractor.js +80 -0
  73. data/app/javascript/panda/cms/editor/editor_js_config.js +177 -0
  74. data/app/javascript/panda/cms/editor/editor_js_initializer.js +285 -0
  75. data/app/javascript/panda/cms/editor/plain_text_editor.js +110 -0
  76. data/app/javascript/panda/cms/editor/resource_loader.js +115 -0
  77. data/app/javascript/panda/cms/tailwindcss-stimulus-components.js +4 -0
  78. data/app/jobs/panda/cms/application_job.rb +6 -0
  79. data/app/jobs/panda/cms/record_visit_job.rb +31 -0
  80. data/app/mailers/panda/cms/application_mailer.rb +8 -0
  81. data/app/mailers/panda/cms/form_mailer.rb +21 -0
  82. data/app/models/action_text/rich_text_version.rb +6 -0
  83. data/app/models/panda/cms/application_record.rb +7 -0
  84. data/app/models/panda/cms/block.rb +34 -0
  85. data/app/models/panda/cms/block_content.rb +18 -0
  86. data/app/models/panda/cms/block_content_version.rb +8 -0
  87. data/app/models/panda/cms/breadcrumb.rb +12 -0
  88. data/app/models/panda/cms/current.rb +17 -0
  89. data/app/models/panda/cms/form.rb +9 -0
  90. data/app/models/panda/cms/form_submission.rb +7 -0
  91. data/app/models/panda/cms/menu.rb +52 -0
  92. data/app/models/panda/cms/menu_item.rb +58 -0
  93. data/app/models/panda/cms/page.rb +96 -0
  94. data/app/models/panda/cms/page_version.rb +8 -0
  95. data/app/models/panda/cms/post.rb +60 -0
  96. data/app/models/panda/cms/post_version.rb +8 -0
  97. data/app/models/panda/cms/redirect.rb +11 -0
  98. data/app/models/panda/cms/template.rb +124 -0
  99. data/app/models/panda/cms/template_version.rb +8 -0
  100. data/app/models/panda/cms/user.rb +31 -0
  101. data/app/models/panda/cms/version.rb +8 -0
  102. data/app/models/panda/cms/visit.rb +9 -0
  103. data/app/services/panda/cms/html_to_editor_js_converter.rb +200 -0
  104. data/app/views/active_storage/blobs/blobs/_blob.html.erb +14 -0
  105. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  106. data/app/views/layouts/panda/cms/application.html.erb +41 -0
  107. data/app/views/layouts/panda/cms/public.html.erb +3 -0
  108. data/app/views/panda/cms/admin/dashboard/show.html.erb +12 -0
  109. data/app/views/panda/cms/admin/files/index.html.erb +124 -0
  110. data/app/views/panda/cms/admin/files/show.html.erb +2 -0
  111. data/app/views/panda/cms/admin/forms/edit.html.erb +0 -0
  112. data/app/views/panda/cms/admin/forms/index.html.erb +13 -0
  113. data/app/views/panda/cms/admin/forms/new.html.erb +15 -0
  114. data/app/views/panda/cms/admin/forms/show.html.erb +35 -0
  115. data/app/views/panda/cms/admin/menus/index.html.erb +8 -0
  116. data/app/views/panda/cms/admin/pages/edit.html.erb +36 -0
  117. data/app/views/panda/cms/admin/pages/index.html.erb +22 -0
  118. data/app/views/panda/cms/admin/pages/new.html.erb +15 -0
  119. data/app/views/panda/cms/admin/pages/show.html.erb +1 -0
  120. data/app/views/panda/cms/admin/posts/_form.html.erb +29 -0
  121. data/app/views/panda/cms/admin/posts/edit.html.erb +6 -0
  122. data/app/views/panda/cms/admin/posts/index.html.erb +18 -0
  123. data/app/views/panda/cms/admin/posts/new.html.erb +6 -0
  124. data/app/views/panda/cms/admin/sessions/new.html.erb +17 -0
  125. data/app/views/panda/cms/admin/settings/bulk_editor/new.html.erb +68 -0
  126. data/app/views/panda/cms/admin/settings/index.html.erb +21 -0
  127. data/app/views/panda/cms/admin/settings/insta.html +4 -0
  128. data/app/views/panda/cms/admin/shared/_breadcrumbs.html.erb +28 -0
  129. data/app/views/panda/cms/admin/shared/_flash.html.erb +5 -0
  130. data/app/views/panda/cms/admin/shared/_sidebar.html.erb +41 -0
  131. data/app/views/panda/cms/form_mailer/notification_email.html.erb +11 -0
  132. data/app/views/panda/cms/shared/_editor.html.erb +0 -0
  133. data/app/views/panda/cms/shared/_favicons.html.erb +9 -0
  134. data/app/views/panda/cms/shared/_footer.html.erb +2 -0
  135. data/app/views/panda/cms/shared/_header.html.erb +15 -0
  136. data/app/views/panda/cms/shared/_importmap.html.erb +33 -0
  137. data/config/importmap.rb +13 -0
  138. data/config/initializers/inflections.rb +3 -0
  139. data/config/initializers/panda/cms/form_errors.rb +38 -0
  140. data/config/initializers/panda/cms/healthcheck_log_silencer.rb +11 -0
  141. data/config/initializers/panda/cms/paper_trail.rb +7 -0
  142. data/config/initializers/panda/cms.rb +10 -0
  143. data/config/initializers/zeitwork.rb +3 -0
  144. data/config/locales/en.yml +49 -0
  145. data/config/puma/test.rb +9 -0
  146. data/config/routes.rb +48 -0
  147. data/config/tailwind.config.js +37 -0
  148. data/db/migrate/20240205223709_create_panda_cms_pages.rb +9 -0
  149. data/db/migrate/20240219213327_create_panda_cms_page_versions.rb +14 -0
  150. data/db/migrate/20240303002805_create_panda_cms_templates.rb +11 -0
  151. data/db/migrate/20240303003434_create_panda_cms_template_versions.rb +14 -0
  152. data/db/migrate/20240303022441_create_panda_cms_blocks.rb +13 -0
  153. data/db/migrate/20240303024256_create_panda_cms_block_contents.rb +10 -0
  154. data/db/migrate/20240303024746_create_panda_cms_block_content_versions.rb +14 -0
  155. data/db/migrate/20240303233238_add_panda_cms_menu_table.rb +10 -0
  156. data/db/migrate/20240303234724_add_panda_cms_menu_item_table.rb +12 -0
  157. data/db/migrate/20240304134343_add_parent_id_to_panda_cms_pages.rb +5 -0
  158. data/db/migrate/20240305000000_convert_html_content_to_editor_js.rb +82 -0
  159. data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +9 -0
  160. data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +16 -0
  161. data/db/migrate/20240316212822_add_kind_to_panda_cms_menus.rb +6 -0
  162. data/db/migrate/20240316221425_add_start_page_to_panda_cms_menus.rb +5 -0
  163. data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +24 -0
  164. data/db/migrate/20240317010532_create_panda_cms_users.rb +12 -0
  165. data/db/migrate/20240317161534_add_max_uses_to_panda_cms_template.rb +7 -0
  166. data/db/migrate/20240317163053_reset_counter_cache_on_panda_cms_template.rb +5 -0
  167. data/db/migrate/20240317214827_create_panda_cms_redirects.rb +14 -0
  168. data/db/migrate/20240317230622_create_panda_cms_visits.rb +13 -0
  169. data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +58 -0
  170. data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +5 -0
  171. data/db/migrate/20240701225422_add_service_name_to_active_storage_blobs.active_storage.rb +22 -0
  172. data/db/migrate/20240701225423_create_active_storage_variant_records.active_storage.rb +28 -0
  173. data/db/migrate/20240701225424_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +8 -0
  174. data/db/migrate/20240804235210_create_panda_cms_forms.rb +11 -0
  175. data/db/migrate/20240805013612_create_panda_cms_form_submissions.rb +9 -0
  176. data/db/migrate/20240805121123_create_panda_cms_posts.rb +27 -0
  177. data/db/migrate/20240805123104_create_panda_cms_post_versions.rb +14 -0
  178. data/db/migrate/20240806112735_fix_panda_cms_visits_column_names.rb +13 -0
  179. data/db/migrate/20240806204412_add_completion_path_to_panda_cms_forms.rb +5 -0
  180. data/db/migrate/20240820081917_change_form_submissions_to_submission_count.rb +5 -0
  181. data/db/migrate/20240904200605_create_action_text_tables.action_text.rb +24 -0
  182. data/db/migrate/20240923234535_add_depth_to_panda_cms_menus.rb +11 -0
  183. data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +5 -0
  184. data/db/migrate/20241119214548_convert_post_content_to_editor_js.rb +35 -0
  185. data/db/migrate/20241119214549_remove_action_text_from_posts.rb +9 -0
  186. data/db/migrate/20241120000419_remove_post_tag_references.rb +19 -0
  187. data/db/migrate/20241120110943_add_editor_js_to_posts.rb +27 -0
  188. data/db/migrate/20241120113859_add_cached_content_to_panda_cms_posts.rb +5 -0
  189. data/db/migrate/20241123234140_remove_post_tag_id_from_posts.rb +5 -0
  190. data/db/migrate/migrate +1 -0
  191. data/db/seeds.rb +5 -0
  192. data/lib/generators/panda/cms/install_generator.rb +29 -0
  193. data/lib/panda/cms/bulk_editor.rb +171 -0
  194. data/lib/panda/cms/demo_site_generator.rb +67 -0
  195. data/lib/panda/cms/editor_js/blocks/alert.rb +34 -0
  196. data/lib/panda/cms/editor_js/blocks/base.rb +33 -0
  197. data/lib/panda/cms/editor_js/blocks/header.rb +15 -0
  198. data/lib/panda/cms/editor_js/blocks/image.rb +36 -0
  199. data/lib/panda/cms/editor_js/blocks/list.rb +32 -0
  200. data/lib/panda/cms/editor_js/blocks/paragraph.rb +15 -0
  201. data/lib/panda/cms/editor_js/blocks/quote.rb +41 -0
  202. data/lib/panda/cms/editor_js/blocks/table.rb +50 -0
  203. data/lib/panda/cms/editor_js/renderer.rb +124 -0
  204. data/lib/panda/cms/editor_js.rb +16 -0
  205. data/lib/panda/cms/editor_js_content.rb +21 -0
  206. data/lib/panda/cms/engine.rb +257 -0
  207. data/lib/panda/cms/exceptions_app.rb +26 -0
  208. data/lib/panda/cms/railtie.rb +11 -0
  209. data/lib/panda/cms/slug.rb +24 -0
  210. data/lib/panda/cms.rb +0 -0
  211. data/lib/panda-cms/version.rb +5 -0
  212. data/lib/panda-cms.rb +81 -0
  213. data/lib/tasks/panda_cms.rake +54 -0
  214. data/lib/templates/erb/scaffold/_form.html.erb.tt +43 -0
  215. data/lib/templates/erb/scaffold/edit.html.erb.tt +8 -0
  216. data/lib/templates/erb/scaffold/index.html.erb.tt +14 -0
  217. data/lib/templates/erb/scaffold/new.html.erb.tt +7 -0
  218. data/lib/templates/erb/scaffold/partial.html.erb.tt +22 -0
  219. data/lib/templates/erb/scaffold/show.html.erb.tt +15 -0
  220. data/public/panda-cms-assets/favicons/android-chrome-192x192.png +0 -0
  221. data/public/panda-cms-assets/favicons/android-chrome-512x512.png +0 -0
  222. data/public/panda-cms-assets/favicons/apple-touch-icon.png +0 -0
  223. data/public/panda-cms-assets/favicons/browserconfig.xml +9 -0
  224. data/public/panda-cms-assets/favicons/favicon-16x16.png +0 -0
  225. data/public/panda-cms-assets/favicons/favicon-32x32.png +0 -0
  226. data/public/panda-cms-assets/favicons/favicon.ico +0 -0
  227. data/public/panda-cms-assets/favicons/mstile-150x150.png +0 -0
  228. data/public/panda-cms-assets/favicons/safari-pinned-tab.svg +61 -0
  229. data/public/panda-cms-assets/favicons/site.webmanifest +14 -0
  230. data/public/panda-cms-assets/panda-logo-screenprint.png +0 -0
  231. data/public/panda-cms-assets/panda-nav.png +0 -0
  232. data/public/panda-cms-assets/rich_text_editor.css +568 -0
  233. metadata +654 -0
@@ -0,0 +1,31 @@
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
@@ -0,0 +1,8 @@
1
+ module Panda
2
+ module CMS
3
+ class Version < ApplicationRecord
4
+ include PaperTrail::VersionConcern
5
+ self.abstract_class = true
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ module Panda
2
+ module CMS
3
+ class Visit < ApplicationRecord
4
+ belongs_to :page, class_name: "Panda::CMS::Page", foreign_key: :page_id, optional: true
5
+ belongs_to :user, class_name: "Panda::CMS::User", foreign_key: :user_id, optional: true
6
+ belongs_to :redirect, class_name: "Panda::CMS::Redirect", foreign_key: :redirect_id, optional: true
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,200 @@
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 as a final paragraph
135
+ if current_text.present?
136
+ # Split any remaining text on double line breaks
137
+ paragraphs = current_text.split(/\n\n+/).map(&:strip)
138
+ paragraphs.each do |paragraph|
139
+ blocks << create_paragraph_block(paragraph) if paragraph.present?
140
+ end
141
+ end
142
+
143
+ raise ConversionError, "No valid content blocks found" if blocks.empty?
144
+
145
+ {
146
+ "time" => Time.current.to_i * 1000,
147
+ "blocks" => blocks,
148
+ "version" => "2.28.2"
149
+ }
150
+ rescue Nokogiri::SyntaxError => e
151
+ raise ConversionError, "Invalid HTML syntax: #{e.message}"
152
+ rescue => e
153
+ raise ConversionError, "Conversion failed: #{e.message}"
154
+ end
155
+ end
156
+
157
+ def self.create_paragraph_block(text)
158
+ {
159
+ "type" => "paragraph",
160
+ "data" => {
161
+ "text" => text.strip
162
+ }
163
+ }
164
+ end
165
+
166
+ def self.process_inline_elements(node)
167
+ result = ""
168
+ node.children.each do |child|
169
+ case child.name
170
+ when "br"
171
+ result += "<br>"
172
+ when "text"
173
+ result += child.text
174
+ when "strong", "b"
175
+ result += "<b>#{child.text}</b>"
176
+ when "em", "i"
177
+ result += "<i>#{child.text}</i>"
178
+ when "a"
179
+ href = child["href"]
180
+ text = child.text.strip
181
+ # Handle email links specially
182
+ if href&.start_with?("mailto:")
183
+ email = href.sub("mailto:", "")
184
+ result += "<a href=\"mailto:#{email}\">#{text}</a>"
185
+ else
186
+ result += "<a href=\"#{href}\">#{text}</a>"
187
+ end
188
+ else
189
+ result += if child.text?
190
+ child.text
191
+ else
192
+ child.to_html
193
+ end
194
+ end
195
+ end
196
+ result.strip
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,14 @@
1
+ <figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
2
+ <% if blob.representable? %>
3
+ <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
4
+ <% end %>
5
+
6
+ <figcaption class="attachment__caption">
7
+ <% if caption = blob.try(:caption) %>
8
+ <%= caption %>
9
+ <% else %>
10
+ <span class="attachment__name"><%= blob.filename %></span>
11
+ <span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
12
+ <% end %>
13
+ </figcaption>
14
+ </figure>
@@ -0,0 +1,3 @@
1
+ <div class="trix-content">
2
+ <%= yield -%>
3
+ </div>
@@ -0,0 +1,41 @@
1
+ <%= render "panda/cms/shared/header", html_class: "h-full bg-white" %>
2
+ <div class="flex h-full" id="panda-container">
3
+ <div class="absolute top-0 w-full lg:flex lg:fixed lg:inset-y-0 lg:z-50 lg:flex-col lg:w-72">
4
+ <div class="flex overflow-y-auto flex-col gap-y-5 px-4 pb-4 max-h-16 bg-gradient-to-br lg:max-h-full grow from-dark to-mid" data-transition-enter="transition-all ease-in-out duration-300" data-transition-enter-from="m-h-16" data-transition-enter-to="max-h-full" data-transition-leave="transition-all ease-in-out duration-300" data-transition-leave-from="max-h-full" data-transition-leave-to="max-h-16">
5
+ <%= render "panda/cms/admin/shared/sidebar" %>
6
+ </div>
7
+ </div>
8
+ <div class="flex flex-col flex-1 mt-16 ml-0 lg:mt-0 lg:ml-72" id="panda-inner-container" <% if content_for :sidebar %> data-controller="toggle" data-action="keydown.esc->modal#close" tabindex="-1"<% end %>>
9
+ <section id="panda-main" class="flex flex-row h-full">
10
+ <div class="flex-1 h-full" id="panda-cms-primary-content">
11
+ <%= render "panda/cms/admin/shared/breadcrumbs" %>
12
+ <%= yield %>
13
+ </div>
14
+ <% if content_for :sidebar %>
15
+ <div data-toggle-target="toggleable" class="hidden flex absolute right-0 flex-col h-full bg-white divide-y divide-gray-200 shadow-xl basis-3/12"
16
+ data-transition-enter="transform transition ease-in-out duration-500"
17
+ data-transition-enter-from="translate-x-full"
18
+ data-transition-enter-to="translate-x-0"
19
+ data-transition-leave="transform transition ease-in-out duration-500"
20
+ data-transition-leave-from="translate-x-full"
21
+ data-transition-leave-to="translate-x-0"
22
+ id="slideover">
23
+ <div class="overflow-y-auto flex-1 h-0">
24
+ <div class="py-3 px-4 mb-4 bg-black">
25
+ <div class="flex justify-between items-center">
26
+ <h2 class="text-base font-semibold leading-6 text-white" id="slideover-title"><i class="mr-2 fa-light fa-gear"></i> <%= yield :sidebar_title %> </h2>
27
+ <a href="#" data-action="click->toggle#toggle touch->toggle#toggle"><i class="font-bold text-white fa-regular fa-xmark right"></i></a>
28
+ </div>
29
+ </div>
30
+ <div class="flex flex-col flex-1 justify-between">
31
+ <div class="px-4 space-y-6">
32
+ <%= yield :sidebar %>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ <% end %>
38
+ </section>
39
+ </div>
40
+ </div>
41
+ <%= render "panda/cms/shared/footer" %>
@@ -0,0 +1,3 @@
1
+ <%= render "panda/cms/shared/header", html_class: "h-full bg-gradient-to-br from-dark to-mid" %>
2
+ <%= yield %>
3
+ <%= render "panda/cms/shared/footer" %>
@@ -0,0 +1,12 @@
1
+ <div class="" data-controller="dashboard">
2
+ <%= render Panda::CMS::Admin::ContainerComponent.new do |container| %>
3
+ <% container.with_heading(text: "Dashboard", level: 1) do |heading| %>
4
+ <% heading.with_button(action: :add, text: "Add Page", link: new_admin_page_path) %>
5
+ <% end %>
6
+ <dl class="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-3">
7
+ <%= render Panda::CMS::Admin::StatisticsComponent.new(metric: "Views Today", value: Panda::CMS::Visit.group_by_day(:visited_at, last: 1).count.values.first) %>
8
+ <%= render Panda::CMS::Admin::StatisticsComponent.new(metric: "Views Last Week", value: Panda::CMS::Visit.group_by_week(:visited_at, last: 1).count.values.first) %>
9
+ <%= render Panda::CMS::Admin::StatisticsComponent.new(metric: "Views Last Month", value: Panda::CMS::Visit.group_by_month(:visited_at, last: 1).count.values.first) %>
10
+ </dl>
11
+ <% end %>
12
+ </div>
@@ -0,0 +1,124 @@
1
+ <%= render Panda::CMS::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading(text: "Files", level: 1) %>
3
+
4
+ <% component.with_slideover(title: "File Details") do %>
5
+ <div class="pt-4 pb-16 space-y-6">
6
+ <div>
7
+ <div class="block overflow-hidden w-full rounded-lg aspect-h-7 aspect-w-10">
8
+ <img src="https://images.unsplash.com/photo-1582053433976-25c00369fc93?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=512&q=80" alt="" class="object-cover">
9
+ </div>
10
+ <div class="flex justify-between items-start mt-4">
11
+ <div>
12
+ <h2 class="text-lg font-medium text-gray-900"><span class="sr-only">Details for </span>IMG_4985.HEIC</h2>
13
+ <p class="text-sm font-medium text-gray-500">3.9 MB</p>
14
+ </div>
15
+ <button type="button" class="flex relative justify-center items-center ml-4 w-8 h-8 text-gray-400 bg-white rounded-full hover:text-gray-500 hover:bg-gray-100 focus:ring-2 focus:outline-none focus:ring-panda-dark">
16
+ <span class="absolute -inset-1.5"></span>
17
+ <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
18
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
19
+ </svg>
20
+ <span class="sr-only">Favorite</span>
21
+ </button>
22
+ </div>
23
+ </div>
24
+ <div>
25
+ <h3 class="font-medium text-gray-900">Information</h3>
26
+ <dl class="mt-2 border-t border-b border-gray-200 divide-y divide-gray-200">
27
+ <div class="flex justify-between py-3 text-sm font-medium">
28
+ <dt class="text-gray-500">Uploaded by</dt>
29
+ <dd class="text-gray-900 whitespace-nowrap">Marie Culver</dd>
30
+ </div>
31
+ <div class="flex justify-between py-3 text-sm font-medium">
32
+ <dt class="text-gray-500">Created</dt>
33
+ <dd class="text-gray-900 whitespace-nowrap">June 8, 2020</dd>
34
+ </div>
35
+ <div class="flex justify-between py-3 text-sm font-medium">
36
+ <dt class="text-gray-500">Last modified</dt>
37
+ <dd class="text-gray-900 whitespace-nowrap">June 8, 2020</dd>
38
+ </div>
39
+ <div class="flex justify-between py-3 text-sm font-medium">
40
+ <dt class="text-gray-500">Dimensions</dt>
41
+ <dd class="text-gray-900 whitespace-nowrap">4032 x 3024</dd>
42
+ </div>
43
+ <div class="flex justify-between py-3 text-sm font-medium">
44
+ <dt class="text-gray-500">Resolution</dt>
45
+ <dd class="text-gray-900 whitespace-nowrap">72 x 72</dd>
46
+ </div>
47
+ </dl>
48
+ </div>
49
+ <div>
50
+ <h3 class="font-medium text-gray-900">Description</h3>
51
+ <div class="flex justify-between items-center mt-2">
52
+ <p class="text-sm italic text-gray-500">Add a description to this image.</p>
53
+ <button type="button" class="flex relative justify-center items-center w-8 h-8 text-gray-400 bg-white rounded-full hover:text-gray-500 hover:bg-gray-100 focus:ring-2 focus:outline-none focus:ring-panda-dark">
54
+ <span class="absolute -inset-1.5"></span>
55
+ <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
56
+ <path d="M2.695 14.763l-1.262 3.154a.5.5 0 00.65.65l3.155-1.262a4 4 0 001.343-.885L17.5 5.5a2.121 2.121 0 00-3-3L3.58 13.42a4 4 0 00-.885 1.343z" />
57
+ </svg>
58
+ <span class="sr-only">Add description</span>
59
+ </button>
60
+ </div>
61
+ </div>
62
+ <div>
63
+ <h3 class="font-medium text-gray-900">Shared with</h3>
64
+ <ul role="list" class="mt-2 border-t border-b border-gray-200 divide-y divide-gray-200">
65
+ <li class="flex justify-between items-center py-3">
66
+ <div class="flex items-center">
67
+ <img src="https://images.unsplash.com/photo-1502685104226-ee32379fefbe?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=3&w=1024&h=1024&q=80" alt="" class="w-8 h-8 rounded-full">
68
+ <p class="ml-4 text-sm font-medium text-gray-900">Aimee Douglas</p>
69
+ </div>
70
+ <button type="button" class="ml-6 text-sm font-medium bg-white rounded-md focus:ring-2 focus:ring-offset-2 focus:outline-none text-panda-dark hover:text-panda-dark focus:ring-panda-dark">Remove<span class="sr-only"> Aimee Douglas</span></button>
71
+ </li>
72
+ <li class="flex justify-between items-center py-3">
73
+ <div class="flex items-center">
74
+ <img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixqx=oilqXxSqey&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="" class="w-8 h-8 rounded-full">
75
+ <p class="ml-4 text-sm font-medium text-gray-900">Andrea McMillan</p>
76
+ </div>
77
+ <button type="button" class="ml-6 text-sm font-medium bg-white rounded-md focus:ring-2 focus:ring-offset-2 focus:outline-none text-panda-dark hover:text-panda-dark focus:ring-panda-dark">Remove<span class="sr-only"> Andrea McMillan</span></button>
78
+ </li>
79
+
80
+ <li class="flex justify-between items-center py-2">
81
+ <button type="button" class="flex items-center p-1 -ml-1 bg-white rounded-md focus:ring-2 focus:outline-none group focus:ring-panda-dark">
82
+ <span class="flex justify-center items-center w-8 h-8 text-gray-400 rounded-full border-2 border-gray-300 border-dashed">
83
+ <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
84
+ <path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
85
+ </svg>
86
+ </span>
87
+ <span class="ml-4 text-sm font-medium text-panda-dark group-hover:text-panda-dark">Share</span>
88
+ </button>
89
+ </li>
90
+ </ul>
91
+ </div>
92
+ <div class="flex gap-x-3">
93
+ <button type="button" class="flex-1 py-2 px-3 text-sm font-semibold text-white bg-black rounded-md shadow-sm hover:bg-black focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-panda-dark">Download</button>
94
+ <button type="button" class="flex-1 py-2 px-3 text-sm font-semibold text-gray-900 bg-white rounded-md ring-1 ring-inset shadow-sm hover:bg-gray-50 ring-mid">Delete</button>
95
+ </div>
96
+ </div>
97
+ <% end %>
98
+
99
+ <% component.with_tab_bar do %>
100
+ <%= component.with_tab(name: "Recently Viewed", active: true) %>
101
+ <%= component.with_tab(name: "Recently Added") %>
102
+ <%= component.with_tab(name: "Favourited") %>
103
+ <% end %>
104
+
105
+ <section>
106
+ <h2 id="gallery-heading" class="sr-only">Recently viewed</h2>
107
+ <ul role="list" class="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 md:grid-cols-4 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8">
108
+ <li class="relative">
109
+ <!-- Current: "ring-2 ring-panda-dark ring-offset-2", Default: "focus-within:ring-2 focus-within:ring-panda-dark focus-within:ring-offset-2 focus-within:ring-offset-gray-100" -->
110
+ <div class="block overflow-hidden w-full bg-gray-100 rounded-lg ring-2 ring-offset-2 ring-panda-dark aspect-w-10 aspect-h-7 group">
111
+ <!-- Current: "", Default: "group-hover:opacity-75" -->
112
+ <img src="https://images.unsplash.com/photo-1582053433976-25c00369fc93?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=512&q=80" alt="" class="object-cover pointer-events-none">
113
+ <button type="button" class="absolute inset-0 focus:outline-none">
114
+ <span class="sr-only">View details for IMG_4985.HEIC</span>
115
+ </button>
116
+ </div>
117
+ <p class="block mt-2 text-sm font-medium text-gray-900 pointer-events-none truncate">IMG_4985.HEIC</p>
118
+ <p class="block text-sm font-medium text-gray-500 pointer-events-none">3.9 MB</p>
119
+ </li>
120
+
121
+ <!-- More files... -->
122
+ </ul>
123
+ </section>
124
+ <% end %>
@@ -0,0 +1,2 @@
1
+ <h1>Admin::Files#show</h1>
2
+ <p>Find me in app/views/panda/cms/admin/files/show.html.erb</p>
File without changes
@@ -0,0 +1,13 @@
1
+ <%= render Panda::CMS::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading(text: "Forms", level: 1) do |heading| %>
3
+ <% end %>
4
+
5
+ <%= render Panda::CMS::Admin::TableComponent.new(term: "form", rows: forms) do |table| %>
6
+ <% table.column("Name") { |form| block_link_to form.name, admin_form_path(form) } %>
7
+ <% table.column("Status") { |form| render Panda::CMS::Admin::TagComponent.new(status: :active) } %>
8
+ <% table.column("Last Submission") do |form| %>
9
+ <%= "#{time_ago_in_words(form.form_submissions&.last&.created_at)} ago" if form.form_submissions&.last %>
10
+ <% end %>
11
+ <% table.column("Completion Path", &:completion_path) %>
12
+ <% end %>
13
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <%= render Panda::CMS::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading(text: "Add Page", level: 1) do |heading| %>
3
+ <% end %>
4
+ <%= panda_cms_form_with model: page, url: admin_pages_path, method: :post do |f| %>
5
+ <% options = nested_set_options(Panda::CMS::Page, page) { |i| "#{"-" * i.level} #{i.title} (#{i.path})" } %>
6
+ <div data-controller="slug">
7
+ <input type="hidden" value="<%= Panda::CMS::Current.root %>" data-slug-target="existing_root">
8
+ <%= f.select :parent_id, options, {}, { "data-slug-target": "input_select", "data-action": "change->slug#setPrePath" } %>
9
+ <%= f.text_field :title, { data: { "slug-target": "input_text", action: "focusout->slug#generatePath" } } %>
10
+ <%= f.text_field :path, { data: { prefix: Panda::CMS::Current.root, "slug-target": "output_text" } } %>
11
+ <%= f.collection_select :panda_cms_template_id, Panda::CMS::Template.available, :id, :name %>
12
+ <%= f.button %>
13
+ </div>
14
+ <% end %>
15
+ <% end %>
@@ -0,0 +1,35 @@
1
+ <%= render Panda::CMS::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading(text: form.name, level: 1) do |heading| %>
3
+ <% end %>
4
+
5
+ <div class="overflow-x-auto -mx-4 rounded-lg ring-1 sm:mx-0 sm:rounded-lg ring-mid">
6
+ <table class="min-w-full divide-y divide-gray-300 border-collapse table-auto">
7
+ <thead>
8
+ <tr>
9
+ <% fields.each do |field, title| %>
10
+ <th scope="col" class="py-3.5 px-3 text-sm font-semibold text-left text-gray-900"><%= title %></th>
11
+ <% end %>
12
+ <th scope="col" class="py-3.5 px-3 text-sm font-semibold text-left text-gray-900">Submitted</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody class="bg-white divide-y divide-gray-200">
16
+ <% submissions.each do |submission| %>
17
+ <tr class="relative bg-white cursor-pointer hover:bg-gray-50">
18
+ <% fields.each do |field| %>
19
+ <td class="py-5 px-3 text-sm text-gray-500">
20
+ <% if field[0] == "email" || field[0] == "email_address" %>
21
+ <a href="mailto:<%= submission.data[field[0]] %>" class="border-b border-gray-500 hover:text-gray-900"><%= submission.data[field[0]] %></a>
22
+ <% else %>
23
+ <%= simple_format(submission.data[field[0]]) %>
24
+ <% end %>
25
+ </td>
26
+ <% end %>
27
+ <td class="py-5 px-3 text-sm text-gray-500 whitespace-nowrap">
28
+ <div class="text-gray-500"><%= time_ago_in_words(submission.created_at) %> ago</div>
29
+ </td>
30
+ </tr>
31
+ <% end %>
32
+ </tbody>
33
+ </table>
34
+ </div>
35
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <%= render Panda::CMS::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading(text: "Menus", level: 1) do |heading| %>
3
+ <% end %>
4
+ <%= render Panda::CMS::Admin::TableComponent.new(term: "menu", rows: menus) do |table| %>
5
+ <% table.column("Name") { |menu| link_to menu.name, edit_admin_menu_path(menu) } %>
6
+ <% table.column("Kind") { |menu| render Panda::CMS::Admin::TagComponent.new(status: :active, text: menu.kind.titleize) } %>
7
+ <% end %>
8
+ <% end %>
@@ -0,0 +1,36 @@
1
+ <%= render Panda::CMS::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading(text: "#{page.title}", level: 1) %>
3
+ <% component.with_slideover(title: "Page Details") do %>
4
+ <%= panda_cms_form_with model: page, url: admin_page_path, method: :put do |f| %>
5
+ <%= f.text_field :title, class: "block w-full rounded-md border-0 p-2 text-gray-900 ring-1 ring-inset ring-mid placeholder:text-gray-300 focus:ring-1 focus:ring-inset focus:ring-dark sm:leading-6 hover:pointer" %>
6
+ <%= f.text_field :template, value: template.name, readonly: true, class: "read-only:bg-gray-100 block w-full rounded-md border-0 p-2 text-gray-900 ring-1 ring-inset ring-mid placeholder:text-gray-300 focus:ring-1 focus:ring-inset focus:ring-dark sm:leading-6 hover:pointer" %>
7
+ <%= f.select :status, options_for_select([["Active", "active"], ["Draft", "draft"], ["Hidden", "hidden"], ["Archived", "archived"]], selected: page.status), {}, class: "block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-mid focus:ring-1 focus:ring-inset focus:ring-dark sm:leading-6 hover:pointer" %>
8
+ <%= f.submit "Save" %>
9
+ <% end %>
10
+ <% end %>
11
+ <div id="successMessage" class="hidden">
12
+ <%= render Panda::CMS::Admin::FlashMessageComponent.new(kind: "success", message: "This page was successfully updated!", temporary: false) %>
13
+ </div>
14
+ <div id="errorMessage" class="hidden">
15
+ <%= render Panda::CMS::Admin::FlashMessageComponent.new(kind: "error", message: "There was an error updating this page.", temporary: false) %>
16
+ </div>
17
+ <div class="grid grid-cols-2 mb-4 -mt-5">
18
+ <div>
19
+ <a class="inline-block mb-2 text-sm text-black/60" target="_blank" href="<%= @page.path %>"><%= @page.path %> <i class="ml-2 fa-solid fa-arrow-up-right-from-square"></i></a>
20
+ </div>
21
+ <div class="relative -mt-5">
22
+ <span class="absolute right-0"><%= render Panda::CMS::Admin::ButtonComponent.new(text: "Save Changes", action: :save_inactive, icon: "check", link: "#", size: :regular, id: "saveEditableButton") %></span>
23
+ </div>
24
+ </div>
25
+ <%= content_tag :iframe, nil,
26
+ src: "#{page.path}?embed_id=#{page.id}",
27
+ class: "p-0 m-0 w-full h-full border border-slate-200",
28
+ style: "min-height: 500px; width: 100%; height: 100%;",
29
+ id: "editablePageFrame",
30
+ data: {
31
+ controller: "editor-iframe",
32
+ editor_iframe_page_id_value: @page.id,
33
+ editor_iframe_admin_path_value: "#{admin_dashboard_url}",
34
+ editor_iframe_autosave_value: false
35
+ } %>
36
+ <% end %>
@@ -0,0 +1,22 @@
1
+ <%= render Panda::CMS::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading(text: "Pages", level: 1) do |heading| %>
3
+ <% heading.with_button(action: :add, text: "Add Page", link: new_admin_page_path) %>
4
+ <% end %>
5
+
6
+ <% if root_page %>
7
+ <%= render Panda::CMS::Admin::TableComponent.new(term: "page", rows: root_page.self_and_descendants) do |table| %>
8
+ <% table.column("Name") do |page| %>
9
+ <div class="<%= table_indent(page) %>">
10
+ <%= link_to page.title, edit_admin_page_path(page), class: "block h-full w-full" %>
11
+ <span class="block text-xs text-black/60"><%= page.path %></span>
12
+ </div>
13
+ <% end %>
14
+ <% table.column("Status") { |page| render Panda::CMS::Admin::TagComponent.new(status: page.status) } %>
15
+ <% table.column("Last Updated") { |page| render Panda::CMS::Admin::UserActivityComponent.new(whodunnit_to: page)} %>
16
+ <% end %>
17
+ <% else %>
18
+ <div class="p-6 bg-error/10 text-error rounded-lg">
19
+ <p class="text-base">No homepage (at <code>/</code>) found. Please create a homepage to start building your site.</p>
20
+ </div>
21
+ <% end %>
22
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <%= render Panda::CMS::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading(text: "Add Page", level: 1) do |heading| %>
3
+ <% end %>
4
+ <%= panda_cms_form_with model: page, url: admin_pages_path, method: :post do |f| %>
5
+ <% options = nested_set_options(Panda::CMS::Page, page) { |i| "#{"-" * i.level} #{i.title} (#{i.path})" } %>
6
+ <div data-controller="slug">
7
+ <input type="hidden" value="<%= Panda::CMS::Current.root %>" data-slug-target="existing_root">
8
+ <%= f.select :parent_id, options, {}, { "data-slug-target": "input_select", "data-action": "change->slug#setPrePath" } %>
9
+ <%= f.text_field :title, { data: { "slug-target": "input_text", action: "focusout->slug#generatePath" } } %>
10
+ <%= f.text_field :path, { meta: t(".path.meta"), data: { prefix: Panda::CMS::Current.root, "slug-target": "output_text" } } %>
11
+ <%= f.collection_select :panda_cms_template_id, Panda::CMS::Template.available, :id, :name %>
12
+ <%= f.button "Create Page" %>
13
+ </div>
14
+ <% end %>
15
+ <% end %>
@@ -0,0 +1 @@
1
+ Show page