panda_cms 0.2.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of panda_cms might be problematic. Click here for more details.

Files changed (144) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +72 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/builds/panda_cms.css +1 -0
  5. data/app/assets/config/panda_cms_manifest.js +1 -0
  6. data/app/assets/stylesheets/panda_cms/application.tailwind.css +30 -0
  7. data/app/builders/panda_cms/form_builder.rb +118 -0
  8. data/app/components/panda_cms/admin/button_component.rb +65 -0
  9. data/app/components/panda_cms/admin/container_component.html.erb +13 -0
  10. data/app/components/panda_cms/admin/container_component.rb +11 -0
  11. data/app/components/panda_cms/admin/flash_message_component.html.erb +30 -0
  12. data/app/components/panda_cms/admin/flash_message_component.rb +44 -0
  13. data/app/components/panda_cms/admin/heading_component.rb +38 -0
  14. data/app/components/panda_cms/admin/slideover_component.html.erb +9 -0
  15. data/app/components/panda_cms/admin/slideover_component.rb +13 -0
  16. data/app/components/panda_cms/admin/tab_bar_component.html.erb +35 -0
  17. data/app/components/panda_cms/admin/tab_bar_component.rb +13 -0
  18. data/app/components/panda_cms/grid_component.html.erb +6 -0
  19. data/app/components/panda_cms/grid_component.rb +13 -0
  20. data/app/components/panda_cms/menu_component.html.erb +3 -0
  21. data/app/components/panda_cms/menu_component.rb +18 -0
  22. data/app/components/panda_cms/page_menu_component.html.erb +24 -0
  23. data/app/components/panda_cms/page_menu_component.rb +24 -0
  24. data/app/components/panda_cms/rich_text_component.html.erb +40 -0
  25. data/app/components/panda_cms/rich_text_component.rb +35 -0
  26. data/app/components/panda_cms/text_component.rb +63 -0
  27. data/app/constraints/panda_cms/admin_constraint.rb +16 -0
  28. data/app/controllers/panda_cms/admin/block_contents_controller.rb +42 -0
  29. data/app/controllers/panda_cms/admin/dashboard_controller.rb +15 -0
  30. data/app/controllers/panda_cms/admin/files_controller.rb +17 -0
  31. data/app/controllers/panda_cms/admin/menus_controller.rb +81 -0
  32. data/app/controllers/panda_cms/admin/pages_controller.rb +88 -0
  33. data/app/controllers/panda_cms/admin/sessions_controller.rb +72 -0
  34. data/app/controllers/panda_cms/application_controller.rb +51 -0
  35. data/app/controllers/panda_cms/errors_controller.rb +31 -0
  36. data/app/controllers/panda_cms/pages_controller.rb +33 -0
  37. data/app/helpers/panda_cms/admin/files_helper.rb +4 -0
  38. data/app/helpers/panda_cms/admin/pages_helper.rb +4 -0
  39. data/app/helpers/panda_cms/application_helper.rb +91 -0
  40. data/app/helpers/panda_cms/pages_helper.rb +4 -0
  41. data/app/helpers/panda_cms/theme_helper.rb +16 -0
  42. data/app/javascript/panda_cms/base.js +37 -0
  43. data/app/javascript/panda_cms/controllers/menu_controller.js +19 -0
  44. data/app/javascript/panda_cms/controllers/rich_text_editor_controller.js +59 -0
  45. data/app/javascript/panda_cms/controllers/text_field_update_controller.js +23 -0
  46. data/app/javascript/panda_cms/vendor/stimulus-components-rails-nested-form.js +2 -0
  47. data/app/javascript/panda_cms/vendor/tailwindcss-stimulus-components.js +2 -0
  48. data/app/jobs/panda_cms/application_job.rb +4 -0
  49. data/app/lib/panda_cms/demo_site_generator.rb +70 -0
  50. data/app/lib/panda_cms/slug.rb +21 -0
  51. data/app/mailers/panda_cms/application_mailer.rb +6 -0
  52. data/app/models/panda_cms/application_record.rb +5 -0
  53. data/app/models/panda_cms/block.rb +32 -0
  54. data/app/models/panda_cms/block_content.rb +16 -0
  55. data/app/models/panda_cms/block_content_version.rb +6 -0
  56. data/app/models/panda_cms/breadcrumb.rb +10 -0
  57. data/app/models/panda_cms/current.rb +15 -0
  58. data/app/models/panda_cms/menu.rb +50 -0
  59. data/app/models/panda_cms/menu_item.rb +56 -0
  60. data/app/models/panda_cms/page.rb +86 -0
  61. data/app/models/panda_cms/page_version.rb +6 -0
  62. data/app/models/panda_cms/redirect.rb +9 -0
  63. data/app/models/panda_cms/template.rb +44 -0
  64. data/app/models/panda_cms/template_version.rb +6 -0
  65. data/app/models/panda_cms/user.rb +11 -0
  66. data/app/models/panda_cms/version.rb +6 -0
  67. data/app/models/panda_cms/visit.rb +7 -0
  68. data/app/views/layouts/panda_cms/application.html.erb +68 -0
  69. data/app/views/layouts/panda_cms/public.html.erb +3 -0
  70. data/app/views/panda_cms/admin/dashboard/show.html.erb +8 -0
  71. data/app/views/panda_cms/admin/files/index.html.erb +124 -0
  72. data/app/views/panda_cms/admin/files/show.html.erb +2 -0
  73. data/app/views/panda_cms/admin/menus/_form.html.erb +21 -0
  74. data/app/views/panda_cms/admin/menus/_menu_item_fields.html.erb +7 -0
  75. data/app/views/panda_cms/admin/menus/edit.html.erb +58 -0
  76. data/app/views/panda_cms/admin/menus/index.html.erb +32 -0
  77. data/app/views/panda_cms/admin/menus/new.html.erb +5 -0
  78. data/app/views/panda_cms/admin/pages/edit.html.erb +35 -0
  79. data/app/views/panda_cms/admin/pages/index.html.erb +46 -0
  80. data/app/views/panda_cms/admin/pages/new.html.erb +16 -0
  81. data/app/views/panda_cms/admin/pages/show.html.erb +1 -0
  82. data/app/views/panda_cms/admin/sessions/new.html.erb +39 -0
  83. data/app/views/panda_cms/admin/shared/_breadcrumbs.html.erb +25 -0
  84. data/app/views/panda_cms/admin/shared/_flash.html.erb +5 -0
  85. data/app/views/panda_cms/admin/shared/_sidebar.html.erb +29 -0
  86. data/app/views/panda_cms/shared/_favicons.html.erb +9 -0
  87. data/app/views/panda_cms/shared/_footer.html.erb +2 -0
  88. data/app/views/panda_cms/shared/_header.html.erb +19 -0
  89. data/config/importmap.rb +7 -0
  90. data/config/initializers/panda_cms/form_errors.rb +38 -0
  91. data/config/initializers/panda_cms.rb +42 -0
  92. data/config/locales/en.yml +13 -0
  93. data/config/routes.rb +26 -0
  94. data/config/tailwind.config.js +31 -0
  95. data/config/tailwind.embed.config.js +20 -0
  96. data/db/migrate/20240205223709_create_panda_cms_pages.rb +9 -0
  97. data/db/migrate/20240219213327_create_panda_cms_page_versions.rb +14 -0
  98. data/db/migrate/20240303002805_create_panda_cms_templates.rb +11 -0
  99. data/db/migrate/20240303003434_create_panda_cms_template_versions.rb +14 -0
  100. data/db/migrate/20240303022441_create_panda_cms_blocks.rb +13 -0
  101. data/db/migrate/20240303024256_create_panda_cms_block_contents.rb +10 -0
  102. data/db/migrate/20240303024746_create_panda_cms_block_content_versions.rb +14 -0
  103. data/db/migrate/20240303233238_add_panda_cms_menu_table.rb +10 -0
  104. data/db/migrate/20240303234724_add_panda_cms_menu_item_table.rb +12 -0
  105. data/db/migrate/20240304134343_add_parent_id_to_panda_cms_pages.rb +5 -0
  106. data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +16 -0
  107. data/db/migrate/20240316212822_add_kind_to_panda_cms_menus.rb +6 -0
  108. data/db/migrate/20240316221425_add_start_page_to_panda_cms_menus.rb +5 -0
  109. data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +24 -0
  110. data/db/migrate/20240317010532_create_panda_cms_users.rb +12 -0
  111. data/db/migrate/20240317161534_add_max_uses_to_panda_cms_template.rb +7 -0
  112. data/db/migrate/20240317163053_reset_counter_cache_on_panda_cms_template.rb +5 -0
  113. data/db/migrate/20240317214827_create_panda_cms_redirects.rb +15 -0
  114. data/db/migrate/20240317230622_create_panda_cms_visits.rb +13 -0
  115. data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +58 -0
  116. data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +5 -0
  117. data/db/seeds.rb +4 -0
  118. data/lib/generators/panda_cms/install_generator.rb +32 -0
  119. data/lib/panda_cms/engine.rb +172 -0
  120. data/lib/panda_cms/exceptions_app.rb +24 -0
  121. data/lib/panda_cms/version.rb +3 -0
  122. data/lib/panda_cms.rb +14 -0
  123. data/lib/tasks/panda_cms.rake +67 -0
  124. data/lib/templates/erb/scaffold/_form.html.erb.tt +43 -0
  125. data/lib/templates/erb/scaffold/edit.html.erb.tt +8 -0
  126. data/lib/templates/erb/scaffold/index.html.erb.tt +14 -0
  127. data/lib/templates/erb/scaffold/new.html.erb.tt +7 -0
  128. data/lib/templates/erb/scaffold/partial.html.erb.tt +22 -0
  129. data/lib/templates/erb/scaffold/show.html.erb.tt +15 -0
  130. data/public/panda-cms-assets/android-chrome-192x192.png +0 -0
  131. data/public/panda-cms-assets/android-chrome-512x512.png +0 -0
  132. data/public/panda-cms-assets/apple-touch-icon.png +0 -0
  133. data/public/panda-cms-assets/browserconfig.xml +9 -0
  134. data/public/panda-cms-assets/editable.js +212 -0
  135. data/public/panda-cms-assets/favicon-16x16.png +0 -0
  136. data/public/panda-cms-assets/favicon-32x32.png +0 -0
  137. data/public/panda-cms-assets/favicon.ico +0 -0
  138. data/public/panda-cms-assets/mstile-150x150.png +0 -0
  139. data/public/panda-cms-assets/panda-logo-screenprint.png +0 -0
  140. data/public/panda-cms-assets/panda-nav.png +0 -0
  141. data/public/panda-cms-assets/safari-pinned-tab.svg +61 -0
  142. data/public/panda-cms-assets/site.webmanifest +14 -0
  143. data/public/panda-cms-assets/stimulus-loading.js +113 -0
  144. metadata +845 -0
@@ -0,0 +1,91 @@
1
+ module PandaCms
2
+ module ApplicationHelper
3
+ def title_tag
4
+ PandaCms.title
5
+ end
6
+
7
+ def panda_cms_editor
8
+ if Current.user&.admin
9
+ content_tag(:a, "🐼", href: edit_admin_page_url(Current.page), class: "text-3xl inline absolute right-2 top-2")
10
+ end
11
+ end
12
+
13
+ def active_link?(path, match: :starts_with)
14
+ if match == :starts_with
15
+ return request.path.starts_with?(path)
16
+ elsif match == :exact
17
+ return (request.path == path)
18
+ end
19
+
20
+ false
21
+ end
22
+
23
+ def panda_cms_form_with(**options, &)
24
+ options[:builder] = PandaCms::FormBuilder
25
+ options[:class] ||= " bg-white border border-gray-300 rounded-md p-6"
26
+ form_with(**options, &)
27
+ end
28
+
29
+ def nav_class(mode)
30
+ if mode == "mobile"
31
+ "-mx-3 block rounded-lg px-3 py-2 font-semibold leading-6 text-white hover:text-white hover:underline focus:underline"
32
+ else
33
+ "font-semibold leading-6 text-white hover:text-white hover:underline focus:underline"
34
+ end
35
+ end
36
+
37
+ def selected_nav_highlight_colour_classes(request)
38
+ "bg-pink-400 border border-white border-opacity-10 shadow-md text-white relative flex py-3 px-2 mb-2 rounded-md group flex gap-x-3 rounded-md text-base leading-6 font-medium "
39
+ end
40
+
41
+ def nav_highlight_colour_classes(request)
42
+ "text-sky-100 hover:bg-gray-700 hover:text-white group flex gap-x-3 py-3 px-2 mb-2 rounded-md text-base leading-6 font-medium "
43
+ end
44
+
45
+ def table_indent(item_with_level_attribute)
46
+ case item_with_level_attribute.level
47
+ when 0
48
+ "ml-0"
49
+ when 1
50
+ "ml-4"
51
+ when 2
52
+ "ml-8"
53
+ when 3
54
+ "ml-12"
55
+ when 4
56
+ "ml-16"
57
+ when 5
58
+ "ml-20"
59
+ when 6
60
+ "ml-24"
61
+ when 7
62
+ "ml-28"
63
+ when 8
64
+ "ml-32"
65
+ when 9
66
+ "ml-36"
67
+ when 10
68
+ "ml-40" # We can go to 72...
69
+ else
70
+ "ml-48"
71
+ end
72
+ end
73
+
74
+ def menu_indent(item_with_level_attribute)
75
+ case item_with_level_attribute.level
76
+ when 0
77
+ "pl-0"
78
+ when 1
79
+ "pl-4"
80
+ when 2
81
+ "pl-8"
82
+ when 3
83
+ "pl-12"
84
+ when 4
85
+ "pl-16"
86
+ else
87
+ "pl-20"
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,4 @@
1
+ module PandaCms
2
+ module PagesHelper
3
+ end
4
+ end
@@ -0,0 +1,16 @@
1
+ module PandaCms
2
+ module ThemeHelper
3
+ # TODO: Move these into one method?
4
+ def h1(text, icon: "", additional_styles: "")
5
+ render HeadingComponent.new(text: text, level: 1, icon: icon, additional_styles: additional_styles)
6
+ end
7
+
8
+ def h2(text, icon: "", additional_styles: "")
9
+ render HeadingComponent.new(text: text, level: 2, icon: icon, additional_styles: additional_styles)
10
+ end
11
+
12
+ def h3(text, icon: "", additional_styles: "")
13
+ render HeadingComponent.new(text: text, level: 3, icon: icon, additional_styles: additional_styles)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,37 @@
1
+ import { Application as PandaCmsApplication } from "@hotwired/stimulus";
2
+
3
+ const panda_cms = PandaCmsApplication.start();
4
+
5
+ // Configure Stimulus development experience
6
+ panda_cms.debug = location.hostname === "localhost";
7
+ window.pandaStimulus = panda_cms;
8
+
9
+ export { panda_cms };
10
+
11
+ // Eager load all controllers defined in the import map under controllers/**/*_controller
12
+ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading";
13
+ eagerLoadControllersFrom("panda_cms/controllers", panda_cms);
14
+
15
+ import {
16
+ Alert,
17
+ Autosave,
18
+ ColorPreview,
19
+ Dropdown,
20
+ Modal,
21
+ Tabs,
22
+ Popover,
23
+ Toggle,
24
+ Slideover,
25
+ } from "panda_cms/vendor/tailwindcss-stimulus-components";
26
+ panda_cms.register("alert", Alert);
27
+ panda_cms.register("autosave", Autosave);
28
+ panda_cms.register("color-preview", ColorPreview);
29
+ panda_cms.register("dropdown", Dropdown);
30
+ panda_cms.register("modal", Modal);
31
+ panda_cms.register("popover", Popover);
32
+ panda_cms.register("slideover", Slideover);
33
+ panda_cms.register("tabs", Tabs);
34
+ panda_cms.register("toggle", Toggle);
35
+
36
+ import RailsNestedForm from "panda_cms/vendor/stimulus-components-rails-nested-form";
37
+ panda_cms.register("nested-form", RailsNestedForm);
@@ -0,0 +1,19 @@
1
+ import { Controller as PandaCmsController } from "@hotwired/stimulus"
2
+
3
+ export default class extends PandaCmsController {
4
+ static targets = ["pandaCmsMenu"]
5
+ static values = {
6
+ open: { type: Boolean, default: false }
7
+ }
8
+
9
+ toggle(event) {
10
+ this.openValue = !this.openValue
11
+ this.animate()
12
+ }
13
+
14
+ animate() {
15
+ this.toggleableTargets.forEach(target => {
16
+ transition(target, this.openValue)
17
+ })
18
+ }
19
+ }
@@ -0,0 +1,59 @@
1
+ import { Controller as PandaCmsController } from "@hotwired/stimulus";
2
+
3
+ export default class extends PandaCmsController {
4
+ static targets = ["container", "hidden", "toolbar", "form"];
5
+
6
+ connect() {
7
+ this.quillInit();
8
+ }
9
+
10
+ /**
11
+ * Fire up quill wyswig editor
12
+ */
13
+ quillInit() {
14
+ const quill = new Quill(this.containerTarget, this.quillOption);
15
+ let _this = this;
16
+
17
+ // While we type, copy the text to our hidden form field so it can be saved.
18
+ quill.on("text-change", function (delta) {
19
+ _this.hiddenTarget.value = quill.root.innerHTML;
20
+ });
21
+
22
+ // Capture focus on and off events
23
+ quill.on("selection-change", function (range, oldRange, source) {
24
+ if (range === null && oldRange !== null) {
25
+ _this.onFocusOut();
26
+ } else if (range !== null && oldRange === null) _this.onFocus();
27
+ });
28
+ }
29
+
30
+ /**
31
+ * Fires when the editor receives focus
32
+ */
33
+ onFocus() {
34
+ // Add a border and reveal the toolbar
35
+ this.element.classList.add("border-gray-200");
36
+ this.toolbarTarget.classList.add("opacity-100");
37
+ }
38
+
39
+ /**
40
+ * Fires when the editor loses focus
41
+ */
42
+ onFocusOut() {
43
+ // Hide the border and toolbar
44
+ this.element.classList.remove("border-gray-200");
45
+ this.toolbarTarget.classList.remove("opacity-100");
46
+
47
+ // Submit the form to save our updates
48
+ this.formTarget.requestSubmit();
49
+ }
50
+
51
+ // Quill configuration options
52
+ get quillOption() {
53
+ return {
54
+ modules: {
55
+ toolbar: this.toolbarTarget,
56
+ },
57
+ };
58
+ }
59
+ }
@@ -0,0 +1,23 @@
1
+ import { Controller as PandaCmsController } from "@hotwired/stimulus"
2
+ export default class extends PandaCmsController {
3
+ static targets = [ "existing_root", "input_select", "input_text", "output_text" ]
4
+
5
+ connect() {
6
+ }
7
+
8
+ generatePath() {
9
+ this.output_textTarget.value = "/" + this.createSlug(this.input_textTarget.value);
10
+ }
11
+
12
+ setPrePath() {
13
+ this.parent_slugs = this.input_selectTarget.options[this.input_selectTarget.selectedIndex].text.match(/.*\((.*)\)$/)[1];
14
+ this.output_textTarget.previousSibling.innerHTML = (this.existing_rootTarget.value + this.parent_slugs).replace(/\/$/, '');;
15
+ }
16
+
17
+ createSlug(input) {
18
+ return input.toLowerCase().trim()
19
+ .replace(/[^\w\s-]/g, "-")
20
+ .replace(/&/, "and")
21
+ .replace(/[\s_-]+/g, "-");
22
+ }
23
+ }
@@ -0,0 +1,2 @@
1
+ import{Controller as e}from"@hotwired/stimulus";const t=class _RailsNestedForm extends e{add(e){e.preventDefault();const t=this.templateTarget.innerHTML.replace(/NEW_RECORD/g,(new Date).getTime().toString());this.targetTarget.insertAdjacentHTML("beforebegin",t);const r=new CustomEvent("rails-nested-form:add",{bubbles:!0});this.element.dispatchEvent(r)}remove(e){e.preventDefault();const t=e.target.closest(this.wrapperSelectorValue);if(t.dataset.newRecord==="true")t.remove();else{t.style.display="none";const e=t.querySelector("input[name*='_destroy']");e.value="1"}const r=new CustomEvent("rails-nested-form:remove",{bubbles:!0});this.element.dispatchEvent(r)}};t.targets=["target","template"],t.values={wrapperSelector:{type:String,default:".nested-form-wrapper"}};let r=t;export{r as default};
2
+
@@ -0,0 +1,2 @@
1
+ import{Controller as e}from"@hotwired/stimulus";var t=Object.defineProperty;var V=(e,a,i)=>a in e?t(e,a,{enumerable:!0,configurable:!0,writable:!0,value:i}):e[a]=i;var s=(e,t,a)=>(V(e,typeof t!="symbol"?t+"":t,a),a);async function r(e,t,a={}){t?T(e,a):b(e,a)}async function T(e,t={}){let a=e.dataset.transitionEnter||t.enter||"enter",i=e.dataset.transitionEnterFrom||t.enterFrom||"enter-from",n=e.dataset.transitionEnterTo||t.enterTo||"enter-to",o=e.dataset.toggleClass||t.toggleClass||"hidden";e.classList.add(...a.split(" ")),e.classList.add(...i.split(" ")),e.classList.remove(...n.split(" ")),e.classList.remove(...o.split(" ")),await v(),e.classList.remove(...i.split(" ")),e.classList.add(...n.split(" "));try{await x(e)}finally{e.classList.remove(...a.split(" "))}}async function b(e,t={}){let a=e.dataset.transitionLeave||t.leave||"leave",i=e.dataset.transitionLeaveFrom||t.leaveFrom||"leave-from",n=e.dataset.transitionLeaveTo||t.leaveTo||"leave-to",o=e.dataset.toggleClass||t.toggle||"hidden";e.classList.add(...a.split(" ")),e.classList.add(...i.split(" ")),e.classList.remove(...n.split(" ")),await v(),e.classList.remove(...i.split(" ")),e.classList.add(...n.split(" "));try{await x(e)}finally{e.classList.remove(...a.split(" ")),e.classList.add(...o.split(" "))}}function v(){return new Promise((e=>{requestAnimationFrame((()=>{requestAnimationFrame(e)}))}))}function x(e){return Promise.all(e.getAnimations().map((e=>e.finished)))}var a=class extends e{connect(){setTimeout((()=>{T(this.element)}),this.showDelayValue),this.hasDismissAfterValue&&setTimeout((()=>{this.close()}),this.dismissAfterValue)}close(){b(this.element).then((()=>{this.element.remove()}))}};s(a,"values",{dismissAfter:Number,showDelay:{type:Number,default:0}});var i=class extends e{connect(){this.timeout=null}save(){clearTimeout(this.timeout),this.timeout=setTimeout((()=>{this.statusTarget.textContent=this.submittingTextValue,this.formTarget.requestSubmit()}),this.submitDurationValue)}success(){this.setStatus(this.successTextValue)}error(){this.setStatus(this.errorTextValue)}setStatus(e){this.statusTarget.textContent=e,this.timeout=setTimeout((()=>{this.statusTarget.textContent=""}),this.statusDurationValue)}};s(i,"targets",["form","status"]),s(i,"values",{submitDuration:{type:Number,default:1e3},statusDuration:{type:Number,default:2e3},submittingText:{type:String,default:"Saving..."},successText:{type:String,default:"Saved!"},errorText:{type:String,default:"Unable to save."}});var n=class extends e{update(){this.preview=this.colorTarget.value}set preview(e){this.previewTarget.style[this.styleValue]=e;let t=this._getContrastYIQ(e);this.styleValue==="color"?this.previewTarget.style.backgroundColor=t:this.previewTarget.style.color=t}_getContrastYIQ(e){e=e.replace("#","");let t=128,a=parseInt(e.substr(0,2),16),i=parseInt(e.substr(2,2),16),n=parseInt(e.substr(4,2),16);return(a*299+i*587+n*114)/1e3>=t?"#000":"#fff"}};s(n,"targets",["preview","color"]),s(n,"values",{style:{type:String,default:"backgroundColor"}});var o=class extends e{connect(){document.addEventListener("turbo:before-cache",this.beforeCache.bind(this))}disconnect(){document.removeEventListener("turbo:before-cache",this.beforeCache.bind(this))}openValueChanged(){r(this.menuTarget,this.openValue,this.transitionOptions),this.openValue===!0&&this.hasMenuItemTarget&&this.menuItemTargets[0].focus()}show(){this.openValue=!0}close(){this.openValue=!1}hide(e){this.closeOnClickOutsideValue&&e.target.nodeType&&this.element.contains(e.target)===!1&&this.openValue&&(this.openValue=!1),this.closeOnEscapeValue&&e.key==="Escape"&&this.openValue&&(this.openValue=!1)}toggle(){this.openValue=!this.openValue}nextItem(e){e.preventDefault(),this.menuItemTargets[this.nextIndex].focus()}previousItem(e){e.preventDefault(),this.menuItemTargets[this.previousIndex].focus()}get currentItemIndex(){return this.menuItemTargets.indexOf(document.activeElement)}get nextIndex(){return Math.min(this.currentItemIndex+1,this.menuItemTargets.length-1)}get previousIndex(){return Math.max(this.currentItemIndex-1,0)}get transitionOptions(){return{enter:this.hasEnterClass?this.enterClass:"transition ease-out duration-100",enterFrom:this.hasEnterFromClass?this.enterFromClass:"transform opacity-0 scale-95",enterTo:this.hasEnterToClass?this.enterToClass:"transform opacity-100 scale-100",leave:this.hasLeaveClass?this.leaveClass:"transition ease-in duration-75",leaveFrom:this.hasLeaveFromClass?this.leaveFromClass:"transform opacity-100 scale-100",leaveTo:this.hasLeaveToClass?this.leaveToClass:"transform opacity-0 scale-95",toggleClass:this.hasToggleClass?this.toggleClass:"hidden"}}beforeCache(){this.openValue=!1,this.menuTarget.classList.add("hidden")}};s(o,"targets",["menu","button","menuItem"]),s(o,"values",{open:{type:Boolean,default:!1},closeOnEscape:{type:Boolean,default:!0},closeOnClickOutside:{type:Boolean,default:!0}}),s(o,"classes",["enter","enterFrom","enterTo","leave","leaveFrom","leaveTo","toggle"]);var l=class extends e{connect(){this.openValue&&this.open(),document.addEventListener("turbo:before-cache",this.beforeCache.bind(this))}disconnect(){document.removeEventListener("turbo:before-cache",this.beforeCache.bind(this))}open(){this.dialogTarget.showModal()}close(){this.dialogTarget.setAttribute("closing",""),Promise.all(this.dialogTarget.getAnimations().map((e=>e.finished))).then((()=>{this.dialogTarget.removeAttribute("closing"),this.dialogTarget.close()}))}backdropClose(e){e.target.nodeName=="DIALOG"&&this.close()}show(){this.dialogTarget.show()}hide(){this.close()}beforeCache(){this.close()}};s(l,"targets",["dialog"]),s(l,"values",{open:Boolean});var h=class extends e{openValueChanged(){r(this.contentTarget,this.openValue),this.shouldAutoDismiss&&this.scheduleDismissal()}show(e){this.shouldAutoDismiss&&this.scheduleDismissal(),this.openValue=!0}hide(){this.openValue=!1}toggle(){this.openValue=!this.openValue}get shouldAutoDismiss(){return this.openValue&&this.hasDismissAfterValue}scheduleDismissal(){this.hasDismissAfterValue&&(this.cancelDismissal(),this.timeoutId=setTimeout((()=>{this.hide(),this.timeoutId=void 0}),this.dismissAfterValue))}cancelDismissal(){typeof this.timeoutId=="number"&&(clearTimeout(this.timeoutId),this.timeoutId=void 0)}};s(h,"targets",["content"]),s(h,"values",{dismissAfter:Number,open:{type:Boolean,default:!1}});var u=class extends e{connect(){this.openValue&&this.open(),document.addEventListener("turbo:before-cache",this.beforeCache.bind(this))}disconnect(){document.removeEventListener("turbo:before-cache",this.beforeCache.bind(this))}open(){this.dialogTarget.showModal()}close(){this.dialogTarget.setAttribute("closing",""),Promise.all(this.dialogTarget.getAnimations().map((e=>e.finished))).then((()=>{this.dialogTarget.removeAttribute("closing"),this.dialogTarget.close()}))}backdropClose(e){e.target.nodeName=="DIALOG"&&this.close()}show(){this.open()}hide(){this.close()}beforeCache(){this.close()}};s(u,"targets",["dialog"]),s(u,"values",{open:Boolean});var c=class extends e{initialize(){this.anchor&&(this.indexValue=this.tabTargets.findIndex((e=>e.id===this.anchor)))}connect(){this.showTab()}change(e){e.currentTarget.tagName==="SELECT"?this.indexValue=e.currentTarget.selectedIndex:e.currentTarget.dataset.index?this.indexValue=e.currentTarget.dataset.index:e.currentTarget.dataset.id?this.indexValue=this.tabTargets.findIndex((t=>t.id==e.currentTarget.dataset.id)):this.indexValue=this.tabTargets.indexOf(e.currentTarget)}nextTab(){this.indexValue=Math.min(this.indexValue+1,this.tabsCount-1)}previousTab(){this.indexValue=Math.max(this.indexValue-1,0)}firstTab(){this.indexValue=0}lastTab(){this.indexValue=this.tabsCount-1}indexValueChanged(){if(this.showTab(),this.dispatch("tab-change",{target:this.tabTargets[this.indexValue],detail:{activeIndex:this.indexValue}}),this.updateAnchorValue){let e=this.tabTargets[this.indexValue].id;if(this.scrollToAnchorValue)location.hash=e;else{let t=window.location.href.split("#")[0]+"#"+e;history.replaceState({},document.title,t)}}}showTab(){this.panelTargets.forEach(((e,t)=>{let a=this.tabTargets[t];t===this.indexValue?(e.classList.remove("hidden"),a.ariaSelected="true",a.dataset.active=!0,this.hasInactiveTabClass&&a?.classList?.remove(...this.inactiveTabClasses),this.hasActiveTabClass&&a?.classList?.add(...this.activeTabClasses)):(e.classList.add("hidden"),a.ariaSelected=null,delete a.dataset.active,this.hasActiveTabClass&&a?.classList?.remove(...this.activeTabClasses),this.hasInactiveTabClass&&a?.classList?.add(...this.inactiveTabClasses))})),this.hasSelectTarget&&(this.selectTarget.selectedIndex=this.indexValue),this.scrollActiveTabIntoViewValue&&this.scrollToActiveTab()}scrollToActiveTab(){let e=this.element.querySelector("[aria-selected]");e&&e.scrollIntoView({inline:"center"})}get tabsCount(){return this.tabTargets.length}get anchor(){return document.URL.split("#").length>1?document.URL.split("#")[1]:null}};s(c,"classes",["activeTab","inactiveTab"]),s(c,"targets",["tab","panel","select"]),s(c,"values",{index:0,updateAnchor:Boolean,scrollToAnchor:Boolean,scrollActiveTabIntoView:Boolean});var d=class extends e{toggle(e){this.openValue=!this.openValue,this.animate()}toggleInput(e){this.openValue=e.target.checked,this.animate()}hide(){this.openValue=!1,this.animate()}show(){this.openValue=!0,this.animate()}animate(){this.toggleableTargets.forEach((e=>{r(e,this.openValue)}))}};s(d,"targets",["toggleable"]),s(d,"values",{open:{type:Boolean,default:!1}});export{a as Alert,i as Autosave,n as ColorPreview,o as Dropdown,l as Modal,h as Popover,u as Slideover,c as Tabs,d as Toggle};
2
+
@@ -0,0 +1,4 @@
1
+ module PandaCms
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,70 @@
1
+ module PandaCms
2
+ class DemoSiteGenerator
3
+ attr_accessor :menus, :pages, :templates
4
+
5
+ def initialize
6
+ @menus = {}
7
+ @pages = {}
8
+ @templates = {}
9
+ end
10
+
11
+ #
12
+ # Creates initial templates and empty blocks
13
+ #
14
+ # @return [Hash] A hash containing the created templates
15
+ def create_templates
16
+ # Templates
17
+ initial_templates = [
18
+ {name: "Homepage", file_path: "layouts/homepage", max_uses: 1},
19
+ {name: "Page", file_path: "layouts/page"}
20
+ ]
21
+
22
+ initial_templates.each do |template|
23
+ @templates[template[:name].downcase.to_sym] = PandaCms::Template.find_or_create_by!(template)
24
+ end
25
+
26
+ # Blocks
27
+ initial_blocks = [
28
+ {kind: "rich_text", name: "Introduction Text", key: "introduction_text", template: @templates[:homepage]},
29
+ {kind: "rich_text", name: "Main Content", key: "main_content", template: @templates[:homepage]},
30
+ {kind: "rich_text", name: "Main Content", key: "main_content", template: @templates[:page]}
31
+ ]
32
+
33
+ initial_blocks.each do |block_data|
34
+ PandaCms::Block.find_or_create_by!(block_data)
35
+ end
36
+
37
+ # Empty block contents
38
+ PandaCms::Block.find_each do |block|
39
+ block.template.pages.each do |page|
40
+ PandaCms::BlockContent.find_or_create_by!(page: page, block: block, content: "")
41
+ end
42
+ end
43
+ end
44
+
45
+ #
46
+ # Creates initial pages
47
+ #
48
+ # @return [Hash] A hash containing the created pages
49
+ def create_pages
50
+ @pages[:home] = PandaCms::Page.find_or_create_by!({path: "/", title: "Home", template: @templates[:homepage]})
51
+ @pages[:about] = PandaCms::Page.find_or_create_by!({path: "/about", title: "About", template: @templates[:page], parent: @pages[:home]})
52
+ @pages[:terms] = PandaCms::Page.find_or_create_by!({path: "/terms-and-conditions", title: "Terms & Conditions", template: @templates[:page], parent: @pages[:home]})
53
+ end
54
+
55
+ #
56
+ # Creates initial menus
57
+ #
58
+ # @return [Hash] A hash containing the created menus
59
+ #
60
+ def create_menus
61
+ @menus = {}
62
+ @menus[:main] = PandaCms::Menu.find_or_create_by!(name: "Main Menu")
63
+ @menus[:footer] = PandaCms::Menu.find_or_create_by!(name: "Footer Menu")
64
+
65
+ # Automatically create main menu from homepage
66
+ @menus[:main].update(kind: :auto, start_page: @pages[:home])
67
+ @menus[:main].generate_auto_menu_items
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,21 @@
1
+ module PandaCms
2
+ class Slug
3
+ # Generates a slug from a provided string
4
+ #
5
+ # @param string [String] The provided string to turn into a slug
6
+ # @return string Generated slug
7
+ # @see text_field_update_controller.js should also implement this logic
8
+ def self.generate(string)
9
+ # Trim whitespace and downcase the string
10
+ string = string.to_s.strip.downcase
11
+ # Replace & with "and"
12
+ string = string.gsub("&", "and")
13
+ # Remove special characters
14
+ string = string.gsub(/[\!\@\£\$\%\^\&\*\(\)\+\=\{\}\[\]\:\;\"\'\|\\\`\<\>\?\,\.\/]+/, "")
15
+ # Replace any whitespace character with -
16
+ string = string.gsub(/[^\w\s-]/, "-")
17
+ # Replace multiple occurences of _ and - with -
18
+ string.gsub(/[\s_-]+/, "-")
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,6 @@
1
+ module PandaCms
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module PandaCms
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,32 @@
1
+ module PandaCms
2
+ class Block < ApplicationRecord
3
+ self.table_name = "panda_cms_blocks"
4
+
5
+ belongs_to :template, foreign_key: :panda_cms_template_id, class_name: "PandaCms::Template", inverse_of: :blocks, optional: true
6
+ has_many :block_contents, foreign_key: :panda_cms_block_id, class_name: "PandaCms::BlockContent", inverse_of: :block
7
+
8
+ validates :kind, presence: true
9
+ validates :name, presence: true
10
+ validates :key, presence: true, uniqueness: {scope: :panda_cms_template_id, case_sensitive: false}
11
+
12
+ # Validation for presence on template intentionally skipped to allow global elements
13
+
14
+ # NB: Commented out values are not yet implemented
15
+ enum :kind, {
16
+ plain_text: "plain_text",
17
+ rich_text: "rich_text",
18
+ iframe: "iframe",
19
+ list: "list"
20
+ # image: "image",
21
+ # video: "video",
22
+ # audio: "audio",
23
+ # file: "file",
24
+ # code: "code",
25
+ # iframe: "iframe",
26
+ # quote: "quote",
27
+ # list: "list"
28
+ # table: "table",
29
+ # form: "form"
30
+ }
31
+ end
32
+ end
@@ -0,0 +1,16 @@
1
+ require "redcarpet"
2
+
3
+ module PandaCms
4
+ class BlockContent < ApplicationRecord
5
+ self.table_name = "panda_cms_block_contents"
6
+
7
+ has_paper_trail versions: {
8
+ class_name: "PandaCms::BlockContentVersion"
9
+ }
10
+
11
+ belongs_to :page, foreign_key: :panda_cms_page_id, class_name: "PandaCms::Page", inverse_of: :block_contents, optional: true, touch: true
12
+ belongs_to :block, foreign_key: :panda_cms_block_id, class_name: "PandaCms::Block", inverse_of: :block_contents, optional: false
13
+
14
+ validates :block, presence: true, uniqueness: {scope: :page}
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ module PandaCms
2
+ class BlockContentVersion < Version
3
+ self.table_name = :panda_cms_block_content_versions
4
+ self.sequence_name = :panda_cms_block_content_versions_id_seq
5
+ end
6
+ end
@@ -0,0 +1,10 @@
1
+ module PandaCms
2
+ class Breadcrumb
3
+ attr_reader :name, :path
4
+
5
+ def initialize(name, path)
6
+ @name = name
7
+ @path = path
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ module PandaCms
2
+ class Current < ActiveSupport::CurrentAttributes
3
+ attribute :root, :page
4
+ attribute :user
5
+ attribute :request_id, :user_agent, :ip_address
6
+
7
+ # resets { Time.zone = nil }
8
+
9
+ # def user=(user)
10
+ # super
11
+ # self.account = user.account
12
+ # Time.zone = user.time_zone
13
+ # end
14
+ end
15
+ end
@@ -0,0 +1,50 @@
1
+ module PandaCms
2
+ class Menu < ApplicationRecord
3
+ self.table_name = "panda_cms_menus"
4
+
5
+ after_save :generate_auto_menu_items, if: -> { kind == "auto" }
6
+
7
+ has_many :menu_items, -> { order(lft: :asc) }, foreign_key: :panda_cms_menu_id, class_name: "PandaCms::MenuItem", inverse_of: :menu
8
+ belongs_to :start_page, class_name: "PandaCms::Page", foreign_key: "start_page_id", inverse_of: :page_menu, optional: true
9
+
10
+ accepts_nested_attributes_for :menu_items, reject_if: :all_blank, allow_destroy: true
11
+
12
+ validates :name, presence: true, uniqueness: {case_sensitive: false}
13
+ validates :kind, presence: true, inclusion: {in: ["static", "auto"]}
14
+ validate :validate_start_page
15
+
16
+ def generate_auto_menu_items
17
+ return false if kind != "auto"
18
+
19
+ # NB: Transactions are not distributed across database connections
20
+ transaction do
21
+ menu_items.destroy_all
22
+ menu_item_root = menu_items.create(text: start_page.title, panda_cms_page_id: start_page.id)
23
+ generate_menu_items(parent_menu_item: menu_item_root, parent_page: start_page)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def generate_menu_items(parent_menu_item:, parent_page:)
30
+ parent_page.children.each do |page|
31
+ menu_item = menu_items.create(text: page.title, panda_cms_page_id: page.id, parent: parent_menu_item)
32
+ if page.children
33
+ generate_menu_items(parent_menu_item: menu_item, parent_page: page)
34
+ end
35
+ end
36
+ end
37
+
38
+ #
39
+ # Validate that the start page is set if the menu is of kind auto
40
+ #
41
+ # @return nil
42
+ # @visibility private
43
+ #
44
+ def validate_start_page
45
+ if kind == "auto" && start_page.nil?
46
+ errors.add(:start_page, "can't be blank")
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,56 @@
1
+ require "awesome_nested_set"
2
+
3
+ module PandaCms
4
+ class MenuItem < ApplicationRecord
5
+ acts_as_nested_set scope: [:panda_cms_menu_id], counter_cache: :children_count
6
+
7
+ self.implicit_order_column = "lft"
8
+ self.table_name = "panda_cms_menu_items"
9
+
10
+ belongs_to :menu, foreign_key: :panda_cms_menu_id, class_name: "PandaCms::Menu", inverse_of: :menu_items, touch: true
11
+ belongs_to :page, foreign_key: :panda_cms_page_id, class_name: "PandaCms::Page", inverse_of: :menu_items, optional: true
12
+
13
+ validates :text, presence: true, uniqueness: {scope: :panda_cms_menu_id, case_sensitive: false}
14
+ validates :page, presence: true, unless: -> { external_url.present? }
15
+ validates :external_url, presence: true, unless: -> { page.present? }
16
+
17
+ validate :validate_is_actual_link
18
+
19
+ #
20
+ # Returns the resolved link for the menu item.
21
+ #
22
+ # If the menu item is associated with a page, it returns the path of the page.
23
+ # If the menu item is associated with an external URL, it returns the external URL.
24
+ #
25
+ # @return [String] Resolved link
26
+ # @visibility public
27
+ def resolved_link
28
+ if page
29
+ page.path
30
+ elsif external_url
31
+ external_url
32
+ else
33
+ ""
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ #
40
+ # Validate that the link is an actual link or a page
41
+ #
42
+ # @return nil
43
+ # @visibility private
44
+ def validate_is_actual_link
45
+ if page.nil? && external_url.nil?
46
+ errors.add(:page, "must be a valid page or external link, neither are set")
47
+ errors.add(:external_url, "must be a valid page or external link, neither are set")
48
+ end
49
+
50
+ if !page.nil? && !external_url.nil?
51
+ errors.add(:page, "must be a valid page or external link, both are set")
52
+ errors.add(:external_url, "must be a valid page or external link, both are set")
53
+ end
54
+ end
55
+ end
56
+ end