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,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class HeadingComponent < ViewComponent::Base
7
+ renders_many :buttons, Panda::CMS::Admin::ButtonComponent
8
+
9
+ attr_reader :text, :level, :icon, :additional_styles
10
+
11
+ def initialize(text:, level: 2, icon: "", additional_styles: "")
12
+ @text = text
13
+ @level = level
14
+ @icon = icon
15
+ @additional_styles = additional_styles
16
+ @additional_styles = @additional_styles.split(" ") if @additional_styles.is_a?(String)
17
+ end
18
+
19
+ def call
20
+ output = ""
21
+ output += content_tag(:div, @text, class: "grow")
22
+
23
+ if buttons?
24
+ output += content_tag(:span, class: "actions flex gap-x-2 -mt-1") do
25
+ safe_join(buttons, "")
26
+ end
27
+ end
28
+
29
+ output = output.html_safe
30
+ base_heading_styles = "flex pt-1 text-black mb-5 -mt-1"
31
+
32
+ if level == 1
33
+ content_tag(:h1, output, class: [base_heading_styles, "text-2xl font-medium", @additional_styles])
34
+ elsif level == 2
35
+ content_tag(:h2, output, class: [base_heading_styles, "text-xl font-medium", @additional_styles])
36
+ elsif level == 3
37
+ content_tag(:h3, output, class: [base_heading_styles, "text-xl", "font-light", @additional_styles])
38
+ elsif level == :panel
39
+ content_tag(:h3, output, class: ["text-base font-medium p-4 text-white"])
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,7 @@
1
+ <div class="col-span-3 mt-5 rounded-lg shadow-md bg-mid shadow-inherit/20">
2
+ <%= heading %>
3
+
4
+ <div class="p-4 text-black bg-white rounded-b-lg">
5
+ <%= content %>
6
+ </div>
7
+ </div>
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class PanelComponent < ViewComponent::Base
7
+ renders_one :heading, ->(text:, icon: "", level: :panel, additional_styles: "") do
8
+ Panda::CMS::Admin::HeadingComponent.new(text: text, icon: icon, level: level, additional_styles: additional_styles)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ <% content_for :sidebar do %>
2
+ <aside class="hidden overflow-y-auto w-96 h-full bg-white lg:block">
3
+ <%= content %>
4
+ </aside>
5
+ <% end %>
6
+
7
+ <% content_for :sidebar_title do %>
8
+ <%= title %>
9
+ <% end %>
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class SlideoverComponent < ViewComponent::Base
7
+ attr_reader :title
8
+
9
+ def initialize(title: "Settings")
10
+ @title = title
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ <div class="overflow-hidden p-4 bg-gradient-to-br rounded-lg border-2 from-light/20 to-light border-mid">
2
+ <dt class="text-base font-medium truncate text-dark"><%= metric %></dt>
3
+ <dd class="mt-1 text-3xl font-medium tracking-tight text-dark"><%= value %></dd>
4
+ </div>
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class StatisticsComponent < ViewComponent::Base
7
+ attr_reader :metric
8
+ attr_reader :value
9
+
10
+ def initialize(metric:, value:)
11
+ @metric = metric
12
+ @value = value
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,35 @@
1
+ <div class="mt-3 sm:mt-2">
2
+ <div class="sm:hidden">
3
+ <label for="tabs" class="sr-only">Select a tab</label>
4
+ <!-- Use an "onChange" listener to redirect the user to the selected tab URL. -->
5
+ <select id="tabs" name="tabs" class="block py-1.5 pr-10 pl-3 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset focus:ring-2 focus:ring-inset ring-mid focus:border-panda-dark focus:ring-panda-dark">
6
+ <% tabs.each do |tab| %>
7
+ <option><%= tab %></option>
8
+ <% end %>
9
+ </select>
10
+ </div>
11
+ <div class="hidden sm:block">
12
+ <div class="flex items-center border-b border-gray-200">
13
+ <nav class="flex flex-1 -mb-px space-x-6 xl:space-x-8" aria-label="Tabs">
14
+ <!-- Current: "border-panda-dark text-panda-dark", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" -->
15
+ <a href="#" aria-current="page" class="py-4 px-1 text-sm font-medium whitespace-nowrap border-b-2 border-panda-dark text-panda-dark">Recently Viewed</a>
16
+ <a href="#" class="py-4 px-1 text-sm font-medium text-gray-500 whitespace-nowrap border-b-2 border-transparent hover:text-gray-700 hover:border-gray-300">Recently Added</a>
17
+ <a href="#" class="py-4 px-1 text-sm font-medium text-gray-500 whitespace-nowrap border-b-2 border-transparent hover:text-gray-700 hover:border-gray-300">Favourited</a>
18
+ </nav>
19
+ <div class="hidden items-center p-0.5 ml-6 bg-gray-100 rounded-lg sm:flex">
20
+ <button type="button" class="p-1.5 text-gray-400 rounded-md hover:bg-white hover:shadow-sm focus:ring-2 focus:ring-inset focus:outline-none focus:ring-panda-dark">
21
+ <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
22
+ <path fill-rule="evenodd" d="M2 3.75A.75.75 0 012.75 3h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 3.75zm0 4.167a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75zm0 4.166a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75zm0 4.167a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75z" clip-rule="evenodd" />
23
+ </svg>
24
+ <span class="sr-only">Use list view</span>
25
+ </button>
26
+ <button type="button" class="p-1.5 ml-0.5 text-gray-400 bg-white rounded-md shadow-sm focus:ring-2 focus:ring-inset focus:outline-none focus:ring-panda-dark">
27
+ <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
28
+ <path fill-rule="evenodd" d="M4.25 2A2.25 2.25 0 002 4.25v2.5A2.25 2.25 0 004.25 9h2.5A2.25 2.25 0 009 6.75v-2.5A2.25 2.25 0 006.75 2h-2.5zm0 9A2.25 2.25 0 002 13.25v2.5A2.25 2.25 0 004.25 18h2.5A2.25 2.25 0 009 15.75v-2.5A2.25 2.25 0 006.75 11h-2.5zm9-9A2.25 2.25 0 0011 4.25v2.5A2.25 2.25 0 0013.25 9h2.5A2.25 2.25 0 0018 6.75v-2.5A2.25 2.25 0 0015.75 2h-2.5zm0 9A2.25 2.25 0 0011 13.25v2.5A2.25 2.25 0 0013.25 18h2.5A2.25 2.25 0 0018 15.75v-2.5A2.25 2.25 0 0015.75 11h-2.5z" clip-rule="evenodd" />
29
+ </svg>
30
+ <span class="sr-only">Use grid view</span>
31
+ </button>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </div>
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class TabBarComponent < ViewComponent::Base
7
+ attr_reader :tabs
8
+
9
+ def initialize(tabs: [])
10
+ @tabs = tabs
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,29 @@
1
+ <div class="table overflow-x-auto mb-12 w-full rounded-lg border border-mid">
2
+ <div class="table-header-group">
3
+ <div class="table-row text-base font-medium text-white bg-mid">
4
+ <% columns.each_with_index do |column, i| %>
5
+ <div class="table-cell sticky top-0 z-10 p-4 <% if i.zero? %>rounded-tl-md<% elsif i == columns.size - 1 %>rounded-tr-md<% end %>"><%= column.label %></div>
6
+ <% end %>
7
+ </div>
8
+ </div>
9
+
10
+ <% if @rows.any? %>
11
+ <div class="table-row-group">
12
+ <% @rows.each do |row| %>
13
+ <div class="table-row relative bg-mid/5 hover:bg-mid/20">
14
+ <% @columns.each do |column| %>
15
+ <div class="table-cell py-5 px-3 h-20 text-sm align-middle whitespace-nowrap border-b border-mid/20">
16
+ <%= view_context.capture(row, &column.cell) %>
17
+ </div>
18
+ <% end %>
19
+ </div>
20
+ <% end %>
21
+ </div>
22
+ </div>
23
+ <% else %>
24
+ </div>
25
+ <div class="text-center mx-12 block border border-dashed py-12 rounded-lg">
26
+ <h3 class="py-1 text-xl font-semibold text-gray-900">No <%= @term.pluralize %></h3>
27
+ <p class="py-1 text-base text-gray-500">Get started by creating a new <%= @term %>.</p>
28
+ </div>
29
+ <% end %>
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class TableComponent < ViewComponent::Base
7
+ attr_reader :columns
8
+
9
+ def initialize(term:, rows:)
10
+ @term = term
11
+ @rows = rows
12
+ @columns = []
13
+ end
14
+
15
+ def column(label, &)
16
+ @columns << Column.new(label, &)
17
+ end
18
+
19
+ private
20
+
21
+ # Ensures @columns gets populated [https://dev.to/rolandstuder/supercharged-table-component-built-with-viewcomponent-3j6i]
22
+ def before_render
23
+ content
24
+ end
25
+ end
26
+
27
+ class Column
28
+ attr_reader :label, :cell
29
+
30
+ def initialize(label, &block)
31
+ @label = label
32
+ @cell = block
33
+ end
34
+ end
35
+
36
+ class TagColumn < Column
37
+ attr_reader :label, :cell
38
+
39
+ def initialize(label, &block)
40
+ @label = label
41
+ @cell = Panda::CMS::Admin::TagComponent.new(status: block)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class TagComponent < ViewComponent::Base
7
+ attr_accessor :status, :text
8
+
9
+ def initialize(status: :active, text: nil)
10
+ @status = status.to_sym
11
+ @text = text || status.to_s.humanize
12
+ end
13
+
14
+ def call
15
+ classes = "inline-flex items-center py-1 px-2 text-xs font-medium rounded-md ring-1 ring-inset "
16
+
17
+ classes += case @status
18
+ when :active
19
+ "text-white ring-black/30 bg-active border-0 "
20
+ when :draft
21
+ "text-black ring-black/30 bg-warning "
22
+ when :inactive, :hidden
23
+ "text-black ring-black/30 bg-black/5 bg-white "
24
+ else
25
+ "text-black bg-white "
26
+ end
27
+
28
+ content_tag :span, class: classes do
29
+ @text
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ <% if user.is_a?(Panda::CMS::User) %>
2
+ <%= render Panda::CMS::Admin::UserDisplayComponent.new(user: user, metadata: "#{time_ago_in_words(time)} ago") %>
3
+ <% elsif time %>
4
+ <div class="text-black/60"><%= time_ago_in_words(time) %> ago</div>
5
+ <% end %>
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class UserActivityComponent < ViewComponent::Base
7
+ attr_accessor :model
8
+ attr_accessor :time
9
+ attr_accessor :user
10
+
11
+ # @param whodunnit_to [ActiveRecord::Base] Model instance to which the user activity is related
12
+ def initialize(whodunnit_to: nil, at: nil, user: nil)
13
+ if whodunnit_to
14
+ @model = whodunnit_to
15
+ whodunnit_id = @model.versions&.last&.whodunnit
16
+ if whodunnit_id
17
+ @user = User.find(whodunnit_id)
18
+ @time = @model.updated_at
19
+ end
20
+ elsif user.is_a?(::Panda::CMS::User) && at.is_a?(::ActiveSupport::TimeWithZone)
21
+ @user = user
22
+ @time = at
23
+ end
24
+
25
+ if !@time
26
+ @user = nil
27
+ @time = nil
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ <div class="block flex-shrink-0 group">
2
+ <div class="flex items-center">
3
+ <% unless user.image_url.empty? %>
4
+ <div>
5
+ <img class="inline-block w-10 h-10 rounded-full" src="<%= user.image_url %>" alt="">
6
+ </div>
7
+ <% else %>
8
+ <div class="inline-block w-10 h-10 bg-gray-200 rounded-full">
9
+ <span class="text-center"><i class="text-mid/50 text-4xl fa-sharp fa-circle-user fa-fw"></i></span>
10
+ </div>
11
+ <% end %>
12
+ <div class="ml-3">
13
+ <p class="text-sm text-black"><%= user.firstname %> <%= user.lastname %></p>
14
+ <% if metadata %><p class="text-sm text-black/60"><%= metadata %></p><% end %>
15
+ </div>
16
+ </div>
17
+ </div>
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class UserDisplayComponent < ViewComponent::Base
7
+ attr_accessor :user_id, :user, :metadata
8
+
9
+ def initialize(user_id: nil, user: nil, metadata: "")
10
+ @user = if user.nil? && user_id.present? && Panda::CMS::User.find(user_id)
11
+ Panda::CMS::User.find(user_id)
12
+ else
13
+ user
14
+ end
15
+
16
+ @metadata = metadata
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ # Text component
6
+ # @param key [Symbol] The key to use for the text component
7
+ # @param text [String] The text to display
8
+ # @param editable [Boolean] If the text is editable or not (defaults to true)
9
+ # @param options [Hash] The options to pass to the content_tag
10
+ class CodeComponent < ViewComponent::Base
11
+ KIND = "code"
12
+
13
+ def initialize(key: :text_component, text: "", editable: true, **options)
14
+ @key = key
15
+ @text = text
16
+ @options = options || {}
17
+ @options[:id] ||= "code-#{key.to_s.dasherize}"
18
+ @editable = editable
19
+
20
+ raise BlockError.new("Key 'code' is not allowed for CodeComponent") if key == :code
21
+ end
22
+
23
+ def call
24
+ # TODO: For the non-editable version, grab this from a cache or similar?
25
+ block = Panda::CMS::Block.find_by(kind: KIND, key: @key, panda_cms_template_id: Current.page.panda_cms_template_id)
26
+
27
+ if block.nil?
28
+ raise Panda::CMS::MissingBlockError.new("Block with key #{@key} not found for page #{Current.page.title}") unless Rails.env.production?
29
+ return false
30
+ end
31
+
32
+ block_content = block.block_contents.find_by(panda_cms_page_id: Current.page.id)
33
+ code_content = block_content&.content.to_s
34
+
35
+ if component_is_editable?
36
+ @options[:contenteditable] = "plaintext-only"
37
+ @options[:data] = {
38
+ "editable-kind": "html",
39
+ "editable-page-id": Current.page.id,
40
+ "editable-block-content-id": block_content&.id
41
+ }
42
+ @options[:class] = "block bg-yellow-50 font-mono p-2 border-2 border-yellow-700"
43
+ @options[:style] = "white-space: pre-wrap;"
44
+
45
+ @options[:id] = "editor-#{block_content&.id}"
46
+ # TODO: Switch between the HTML and the preview?
47
+ content_tag(:div, code_content, @options, true)
48
+ else
49
+ code_content.html_safe
50
+ end
51
+ end
52
+
53
+ def component_is_editable?
54
+ # TODO: Permissions
55
+ @editable && is_embedded? && Current.user&.admin
56
+ end
57
+
58
+ def is_embedded?
59
+ # TODO: Check security on this - embed_id should match something?
60
+ request.params.dig(:embed_id).present?
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,6 @@
1
+ <div class="w-full grid <%= @columns %> min-h-20">
2
+ <% @colspans.each do |colspan| %>
3
+ <div class="border border-red-500 bg-red-50 <%= colspan %>" onDragOver="parent.onDragOver(event);" onDrop="parent.onDrop(event);">
4
+ </div>
5
+ <% end %>
6
+ </div>
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ class GridComponent < ViewComponent::Base
6
+ def initialize(columns: 1, spans: [1])
7
+ @columns = "grid-cols-#{columns}"
8
+ @colspans = []
9
+ spans.each do |span|
10
+ @colspans << "col-span-#{span}"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ <% @menu_items.each do |menu_item| %>
2
+ <a href="<%= menu_item.resolved_link %>" class="<%= menu_item.css_classes %>"><%= menu_item.text %></a>
3
+ <% if @render_page_menu && menu_item.page %>
4
+ <%= render Panda::CMS::PageMenuComponent.new(page: menu_item.page, start_depth: 1, styles: @page_menu_styles, show_heading: false) %>
5
+ <% end %>
6
+ <% end %>
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ class MenuComponent < ViewComponent::Base
6
+ #
7
+ # Renders the menu item and its children
8
+ #
9
+ # @param [String] name The name of the menu
10
+ # @param [String] current_path The current path of the request (request.path)
11
+ # @param [Hash] styles
12
+ # The CSS classes to apply to the menu items, containing "default", "inactive" and "active" keys.
13
+ # The "default" key is applied to all menu items. "inactive" and "active" are set based on the
14
+ # current path.
15
+ # @return [void]
16
+ def initialize(name:, current_path: "", styles: {}, overrides: {}, render_page_menu: false, page_menu_styles: {})
17
+ @menu = Panda::CMS::Menu.find_by(name: name)
18
+ @menu_items = @menu.menu_items
19
+ @menu_items = @menu_items.where("depth <= ?", @menu.depth) if @menu.depth
20
+ @menu_items = @menu_items.order(:lft)
21
+ @current_path = current_path.to_s
22
+ @render_page_menu = render_page_menu
23
+
24
+ @menu_items = @menu_items.order(:lft).map do |menu_item|
25
+ if is_active?(menu_item)
26
+ menu_item.define_singleton_method(:css_classes) { styles[:default] + " " + styles[:active] }
27
+ else
28
+ menu_item.define_singleton_method(:css_classes) { styles[:default] + " " + styles[:inactive] }
29
+ end
30
+
31
+ menu_item
32
+ end
33
+
34
+ # TODO: Surely don't need this but Current.page isn't working in the component
35
+ if @render_page_menu
36
+ @current_page = Panda::CMS::Page.find_by(path: @current_path)
37
+ @page_menu_styles = page_menu_styles
38
+ end
39
+ end
40
+
41
+ def is_active?(menu_item)
42
+ return true if @current_path == "/" && active_link?(menu_item.page.path, match: :exact)
43
+ return true if menu_item.page.path != "/" && active_link?(menu_item.page.path, match: :starts_with)
44
+ false
45
+ end
46
+
47
+ def active_link?(path, match: :starts_with)
48
+ if match == :starts_with
49
+ return @current_path.starts_with?(path)
50
+ elsif match == :exact
51
+ return (@current_path == path)
52
+ end
53
+
54
+ false
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,21 @@
1
+ <nav class="<%= styles[:container] %>">
2
+ <ul role="list" class="p-0 m-0">
3
+ <% if @show_heading %>
4
+ <li>
5
+ <a href="<%= menu_item.page.path %>" class="<%= menu_item.page == Panda::CMS::Current.page ? styles[:current_page_active] : styles[:current_page_inactive] %>">
6
+ <%= menu_item.text %>
7
+ </a>
8
+ </li>
9
+ <% end %>
10
+ <ul>
11
+ <% Panda::CMS::MenuItem.includes(:page).each_with_level(menu_item.descendants) do |submenu_item, level| %>
12
+ <% next if Panda::CMS::Current.page == menu_item.page && level > 1 # If we're on the "top" menu item, only show its direct ancestors %>
13
+ <% next if submenu_item.page&.path[/\:/] %>
14
+ <% next if submenu_item&.page.nil? || Panda::CMS::Current.page.nil? || (submenu_item.page&.depth&.to_i > Panda::CMS::Current.page&.depth&.to_i && !Panda::CMS::Current.page&.in?(submenu_item.page.ancestors)) %>
15
+ <li data-level="<%= level %>" data-page-id="<%= submenu_item.page.id %>" class="<%= submenu_item.page == Panda::CMS::Current.page ? @styles[:active] : @styles[:inactive] %>">
16
+ <a href="<%= submenu_item.page&.path %>" class="<%= helpers.menu_indent(submenu_item, indent_with: @styles[:indent_with]) %>"><%= submenu_item.page&.title %></a>
17
+ </li>
18
+ <% end %>
19
+ </ul>
20
+ </ul>
21
+ </nav>
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ class PageMenuComponent < ViewComponent::Base
6
+ attr_accessor :page
7
+ attr_accessor :menu_item
8
+ attr_accessor :styles
9
+
10
+ def initialize(page:, start_depth:, styles: {}, show_heading: true)
11
+ @page = page
12
+
13
+ unless @page.nil?
14
+ start_page = if @page.depth == start_depth
15
+ @page
16
+ else
17
+ @page.ancestors.find { |anc| anc.depth == start_depth }
18
+ end
19
+
20
+ menu = start_page&.page_menu
21
+ return if menu.nil?
22
+
23
+ @menu_item = menu.menu_items.order(:lft)&.first
24
+
25
+ @show_heading = show_heading
26
+
27
+ # Set some default styles for sanity
28
+ @styles = styles
29
+ @styles[:indent_with] ||= "pl-2"
30
+ end
31
+ end
32
+
33
+ def render?
34
+ @page&.path != "/" && @menu_item.present?
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,6 @@
1
+ <% if @editable %>
2
+ <div class="panda-cms-content" data-editable-previous-data="<%= @content.to_json %>" id="editor-<%= @options[:id] %>" data-editable-kind="rich_text" data-editable-block-content-id="<%= @options[:id] %>" data-editable-page-id="<%= @options[:data][:page_id] %>" style="border: 1px dashed #ccc; outline: none; cursor: pointer; transition: background 500ms linear; background-color: inherit;">
3
+ </div>
4
+ <% else %>
5
+ <div class="panda-cms-content"><%= @content %></div>
6
+ <% end %>