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,32 @@
1
+ module Generators
2
+ module PandaCms
3
+ class InstallGenerator < ::Rails::Generators::Base
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ namespace "panda_cms:install"
7
+ desc "Adds the basic configuration for Panda CMS to your Rails app."
8
+
9
+ def create_initializer_file
10
+ # Add the initializer
11
+ initializer_path = "config/initializers/panda_cms.rb"
12
+ unless File.exist?("#{::Rails.root}/#{initializer_path}")
13
+ FileUtils.cp "#{::PandaCms::Engine.root}/#{initializer_path}", "#{::Rails.root}/#{initializer_path}"
14
+ end
15
+
16
+ # # Add the JavaScript controllers
17
+ # unless File.exist?("#{::Rails.root}/app/javascript/panda_cms/controllers/editable_controller.js")
18
+ # `mkdir -p "#{::Rails.root}/app/javascript/panda_cms/controllers"`
19
+ # # FileUtils.cp "#{::PandaCms::Engine.root}/app/assets/javascript/panda_cms/controllers/editable_controller.js", "#{::Rails.root}/app/javascript/panda_cms/controllers/editable_controller.js"
20
+ # # `bin/importmap pin tailwindcss-stimulus-components`
21
+ # # `bin/importmap pin stimulus-rails-nested-form`
22
+ # end
23
+
24
+ # Add the seed loader to the seeds.rb file
25
+ unless File.read("#{::Rails.root}/db/seeds.rb")&.include?("PandaCms::Engine.load_seed")
26
+ existing_seeds = File.read("#{::Rails.root}/db/seeds.rb")
27
+ IO.write("#{::Rails.root}/db/seeds.rb", "PandaCms::Engine.load_seed\n\n#{existing_seeds}")
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,172 @@
1
+ require "importmap-rails"
2
+ require "paper_trail"
3
+ require "view_component"
4
+
5
+ require "omniauth"
6
+ require "omniauth/rails_csrf_protection"
7
+ require "omniauth/strategies/microsoft_graph"
8
+ require "omniauth/strategies/google_oauth2"
9
+ require "omniauth/strategies/github"
10
+
11
+ require "panda_cms/exceptions_app"
12
+
13
+ module PandaCms
14
+ class Engine < ::Rails::Engine
15
+ isolate_namespace PandaCms
16
+ engine_name "panda_cms"
17
+
18
+ attr_accessor :importmap
19
+
20
+ config.to_prepare do
21
+ ApplicationController.helper(::ApplicationHelper)
22
+ end
23
+
24
+ # We rely on ViewComponent here before we can continue
25
+ config.railties_order = [::ViewComponent::Engine, PandaCms::Engine, :main_app, :all]
26
+
27
+ # Set our generators
28
+ config.generators do |g|
29
+ g.orm :active_record, primary_key_type: :uuid
30
+ g.test_framework :rspec, fixture: true
31
+ g.fixture_replacement :factory_bot, dir: "spec/factories"
32
+ g.view_specs false
33
+ g.templates.unshift File.expand_path("../../templates", __FILE__)
34
+ end
35
+
36
+ # Make files in public available to the main app (e.g. favicon.ico)
37
+ config.app_middleware.use(
38
+ Rack::Static,
39
+ urls: ["/panda-cms-assets"],
40
+ root: PandaCms::Engine.root.join("public")
41
+ )
42
+
43
+ config.exceptions_app = PandaCms::ExceptionsApp.new(exceptions_app: routes)
44
+
45
+ # Create an importmap for the engine's JS
46
+ initializer "panda_cms.importmap", before: "importmap" do |app|
47
+ map = Importmap::Map.new
48
+ map.draw(PandaCms::Engine.root.join("config/importmap.rb"))
49
+ map.cache_sweeper(watches: PandaCms::Engine.root.join("app/javascript"))
50
+ # TODO: Sort in production?
51
+ app.config.assets.paths << PandaCms::Engine.root.join("app/javascript") if Rails.env.development?
52
+ PandaCms::Engine.importmap = map
53
+ ActiveSupport.on_load(:action_controller_base, run_once: true) do
54
+ before_action { PandaCms::Engine.importmap.cache_sweeper.execute_if_updated }
55
+ end
56
+ end
57
+
58
+ # Append routes to the routes file
59
+ config.after_initialize do |app|
60
+ app.routes.append do
61
+ mount PandaCms::Engine => "/", :as => "panda_cms"
62
+ get "/_maintenance", to: "panda_cms/errors#error_503", as: :panda_cms_maintenance
63
+ get "/*path", to: "panda_cms/pages#show", as: :panda_cms_page
64
+ root to: "panda_cms/pages#root"
65
+ end
66
+ end
67
+
68
+ # Add the migrations to the main app
69
+ initializer "panda_cms.migrations" do |app|
70
+ unless app.root.to_s.match root.to_s
71
+ config.paths["db/migrate"].expanded.each do |expanded_path|
72
+ app.config.paths["db/migrate"] << expanded_path
73
+ end
74
+ end
75
+ end
76
+
77
+ # Set up authentication
78
+ initializer "panda_cms.omniauth", before: "omniauth" do |app|
79
+ app.config.session_store :cookie_store, key: "_panda_cms_session"
80
+ app.config.middleware.use ActionDispatch::Cookies
81
+ app.config.middleware.use ActionDispatch::Session::CookieStore, app.config.session_options
82
+
83
+ OmniAuth.config.logger = Rails.logger
84
+ auth_path = "#{PandaCms.admin_path}/auth"
85
+ callback_path = "/callback"
86
+
87
+ # TODO: Move this to somewhere more sensible
88
+ # Define the mapping of our provider "names" to the OmniAuth strategies and configuration
89
+ available_providers = {
90
+ microsoft: {
91
+ strategy: :microsoft_graph,
92
+ defaults: {
93
+ name: "microsoft",
94
+ # Setup at the following URL:
95
+ # https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade
96
+ client_id: Rails.application.credentials.dig(:microsoft, :client_id),
97
+ client_secret: Rails.application.credentials.dig(:microsoft, :client_secret),
98
+ # Don't change this or the sky will fall on your head
99
+ # https://github.com/synth/omniauth-microsoft_graph/tree/main?tab=readme-ov-file#domain-verification
100
+ skip_domain_verification: false,
101
+ # If your application is single-tenanted, replace "common" with your tenant (directory) ID
102
+ # from https://portal.azure.com/#settings/directory, otherwise you'll likely want to leave
103
+ # these settings unchanged
104
+ client_options: {
105
+ site: "https://login.microsoftonline.com/",
106
+ token_url: "common/oauth2/v2.0/token",
107
+ authorize_url: "common/oauth2/v2.0/authorize"
108
+ },
109
+ # If you assign specific users or groups, you will likely want to set this to
110
+ # true to enable auto-provisioning
111
+ create_account_on_first_login: false,
112
+ create_admin_account_on_first_login: false
113
+ }
114
+ },
115
+ google: {
116
+ strategy: :google_oauth2,
117
+ defaults: {
118
+ name: "google",
119
+ # Setup at the following URL: https://console.developers.google.com/
120
+ client_id: Rails.application.credentials.dig(:google, :client_id),
121
+ client_secret: Rails.application.credentials.dig(:google, :client_secret),
122
+ # If you assign specific users or groups, you will likely want to set this to
123
+ # true to enable auto-provisioning
124
+ create_account_on_first_login: false,
125
+ create_admin_account_on_first_login: false,
126
+ # Options we need
127
+ scope: "email, profile",
128
+ image_aspect_ratio: "square",
129
+ image_size: 150
130
+ }
131
+ },
132
+ github: {
133
+ strategy: :github,
134
+ defaults: {
135
+ # Setup at the following URL: https://github.com/settings/applications/new
136
+ # In the meantime, as long as you're set to /admin as your login path, you can
137
+ # use these for a first login
138
+ client_id: Rails.application.credentials.dig(:github, :client_id) || "Ov23li9k0LpMXtq8FShb",
139
+ client_secret: Rails.application.credentials.dig(:github, :client_secret) || "659f54cad324a00b19fc4e342cfecc6a0534fa55",
140
+ name: "github",
141
+ scope: "user:email,read:user",
142
+ create_account_on_first_login: false,
143
+ create_admin_account_on_first_login: false
144
+ }
145
+ }
146
+ }
147
+
148
+ available_providers.each do |provider, options|
149
+ if PandaCms.authentication.dig(provider, :enabled)
150
+ options[:defaults][:path_prefix] = auth_path
151
+ options[:defaults][:redirect_uri] = "#{PandaCms.url}#{auth_path}/#{provider}#{callback_path}"
152
+
153
+ provider_config = options[:defaults].merge(PandaCms.authentication[provider].except(
154
+ :enabled,
155
+ :client_id,
156
+ :client_secret,
157
+ :create_account_on_first_login,
158
+ :create_admin_account_on_first_login
159
+ ))
160
+
161
+ Rails.logger.info("Configuring OmniAuth for #{provider} with #{provider_config}")
162
+
163
+ app.config.middleware.use OmniAuth::Builder do
164
+ provider options[:strategy], provider_config
165
+ end
166
+
167
+ OmniAuth.config.allowed_request_methods = %i[post get] if provider == :google
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,24 @@
1
+ # https://guides.rubyonrails.org/configuring.html#config-exceptions-app
2
+ module PandaCms
3
+ class ExceptionsApp
4
+ def initialize(exceptions_app:)
5
+ @exceptions_app = exceptions_app
6
+ end
7
+
8
+ def call(env)
9
+ request = ActionDispatch::Request.new(env)
10
+
11
+ fallback_to_html_format_if_invalid_mime_type(request)
12
+
13
+ @exceptions_app.call(env)
14
+ end
15
+
16
+ private
17
+
18
+ def fallback_to_html_format_if_invalid_mime_type(request)
19
+ request.formats
20
+ rescue ActionDispatch::Http::MimeNegotiation::InvalidType
21
+ request.set_header "CONTENT_TYPE", "text/html"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ module PandaCms
2
+ VERSION = "0.2.0"
3
+ end
data/lib/panda_cms.rb ADDED
@@ -0,0 +1,14 @@
1
+ require "panda_cms/version"
2
+ require "panda_cms/engine"
3
+
4
+ module PandaCms
5
+ mattr_accessor :admin_path
6
+ mattr_accessor :authentication
7
+ mattr_accessor :require_login_to_view
8
+ mattr_accessor :title
9
+ mattr_accessor :url
10
+
11
+ def self.admin_path_symbol
12
+ @@admin_path.delete_prefix("/")
13
+ end
14
+ end
@@ -0,0 +1,67 @@
1
+ require "tailwindcss-rails"
2
+ require "shellwords"
3
+
4
+ ENV["TAILWIND_PATH"] ||= Tailwindcss::Engine.root.join("exe/tailwindcss").to_s
5
+
6
+ namespace :panda_cms do
7
+ namespace :assets do
8
+ desc "Build admin assets for Panda CMS"
9
+ task :admin do
10
+ run_tailwind(
11
+ root_path: PandaCms::Engine.root,
12
+ input_path: "app/assets/stylesheets/panda_cms/application.tailwind.css",
13
+ output_path: "app/assets/builds/panda_cms.css"
14
+ )
15
+ end
16
+
17
+ desc "Build dummy assets for Panda CMS"
18
+ task :dummy do
19
+ run_tailwind(
20
+ root_path: Rails.application.root,
21
+ input_path: "app/assets/stylesheets/application.tailwind.css",
22
+ output_path: "app/assets/builds/application.css",
23
+ config_path: "config/tailwind.config.js"
24
+ )
25
+ end
26
+
27
+ desc "Watch admin assets for Panda CMS"
28
+ task :watch_admin do
29
+ run_tailwind(
30
+ root_path: PandaCms::Engine.root,
31
+ input_path: "app/assets/stylesheets/panda_cms/application.tailwind.css",
32
+ output_path: "app/assets/builds/panda_cms.css",
33
+ watch: true
34
+ )
35
+ end
36
+
37
+ desc "Watch dummy assets for Panda CMS"
38
+ task :watch_dummy do
39
+ run_tailwind(
40
+ root_path: Rails.application.root,
41
+ input_path: "app/assets/stylesheets/application.tailwind.css",
42
+ output_path: "app/assets/builds/application.css",
43
+ config_path: "config/tailwind.config.js",
44
+ watch: true
45
+ )
46
+ end
47
+ end
48
+ end
49
+
50
+ task default: [:spec, :panda_cms]
51
+
52
+ def run_tailwind(root_path:, input_path: nil, output_path: nil, config_path: nil, watch: false)
53
+ Rails.logger = Logger.new($stdout)
54
+ config_path ||= root_path.join("config/tailwind.config.js")
55
+
56
+ command = [
57
+ ENV["TAILWIND_PATH"],
58
+ "-i #{root_path.join(input_path)}",
59
+ "-o #{root_path.join(output_path)}",
60
+ "-c #{root_path.join(config_path)}",
61
+ "-m"
62
+ ]
63
+
64
+ command << "-w" if watch
65
+
66
+ exec command.join(" ")
67
+ end
@@ -0,0 +1,43 @@
1
+ <%%= form_with(model: <%= model_resource_name %>, class: "contents") do |form| %>
2
+ <%% if <%= singular_table_name %>.errors.any? %>
3
+ <div id="error_explanation" class="py-2 px-3 mt-3 font-medium text-red-500 bg-red-50 rounded-lg">
4
+ <h2><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2>
5
+
6
+ <ul>
7
+ <%% <%= singular_table_name %>.errors.each do |error| %>
8
+ <li><%%= error.full_message %></li>
9
+ <%% end %>
10
+ </ul>
11
+ </div>
12
+ <%% end %>
13
+
14
+ <% attributes.each do |attribute| -%>
15
+ <div class="my-5">
16
+ <% if attribute.password_digest? -%>
17
+ <%%= form.label :password %>
18
+ <%%= form.password_field :password, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
19
+ </div>
20
+
21
+ <div class="my-5">
22
+ <%%= form.label :password_confirmation %>
23
+ <%%= form.password_field :password_confirmation, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
24
+ <% elsif attribute.attachments? -%>
25
+ <%%= form.label :<%= attribute.column_name %> %>
26
+ <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, multiple: true, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
27
+ <% else -%>
28
+ <%%= form.label :<%= attribute.column_name %> %>
29
+ <% if attribute.field_type == :text_area -%>
30
+ <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, rows: 4, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
31
+ <% elsif attribute.field_type == :check_box -%>
32
+ <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, class: "block mt-2 h-5 w-5" %>
33
+ <% else -%>
34
+ <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
35
+ <% end -%>
36
+ <% end -%>
37
+ </div>
38
+
39
+ <% end -%>
40
+ <div class="inline">
41
+ <%%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
42
+ </div>
43
+ <%% end %>
@@ -0,0 +1,8 @@
1
+ <div class="mx-auto w-full md:w-2/3">
2
+ <h1 class="text-4xl font-bold">Editing <%= human_name.downcase %></h1>
3
+
4
+ <%%= render "form", <%= singular_table_name %>: @<%= singular_table_name %> %>
5
+
6
+ <%%= link_to "Show this <%= human_name.downcase %>", @<%= singular_table_name %>, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
7
+ <%%= link_to "Back to <%= human_name.pluralize.downcase %>", <%= index_helper %>_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
8
+ </div>
@@ -0,0 +1,14 @@
1
+ <div class="w-full">
2
+ <%% if notice.present? %>
3
+ <p class="inline-block py-2 px-3 mb-5 font-medium text-green-500 bg-green-50 rounded-lg" id="notice"><%%= notice %></p>
4
+ <%% end %>
5
+
6
+ <div class="flex justify-between items-center">
7
+ <h1 class="text-4xl font-bold"><%= human_name.pluralize %></h1>
8
+ <%%= link_to "New <%= human_name.downcase %>", new_<%= singular_route_name %>_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
9
+ </div>
10
+
11
+ <div id="<%= plural_table_name %>" class="min-w-full">
12
+ <%%= render @<%= plural_table_name %> %>
13
+ </div>
14
+ </div>
@@ -0,0 +1,7 @@
1
+ <div class="mx-auto w-full md:w-2/3">
2
+ <h1 class="text-4xl font-bold">New <%= human_name.downcase %></h1>
3
+
4
+ <%%= render "form", <%= singular_table_name %>: @<%= singular_table_name %> %>
5
+
6
+ <%%= link_to "Back to <%= human_name.pluralize.downcase %>", <%= index_helper %>_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
7
+ </div>
@@ -0,0 +1,22 @@
1
+ <div id="<%%= dom_id <%= singular_name %> %>">
2
+ <% attributes.reject(&:password_digest?).each do |attribute| -%>
3
+ <p class="my-5">
4
+ <strong class="block mb-1 font-medium"><%= attribute.human_name %>:</strong>
5
+ <% if attribute.attachment? -%>
6
+ <%%= link_to <%= singular_name %>.<%= attribute.column_name %>.filename, <%= singular_name %>.<%= attribute.column_name %> if <%= singular_name %>.<%= attribute.column_name %>.attached? %>
7
+ <% elsif attribute.attachments? -%>
8
+ <%% <%= singular_name %>.<%= attribute.column_name %>.each do |<%= attribute.singular_name %>| %>
9
+ <div><%%= link_to <%= attribute.singular_name %>.filename, <%= attribute.singular_name %> %></div>
10
+ <%% end %>
11
+ <% else -%>
12
+ <%%= <%= singular_name %>.<%= attribute.column_name %> %>
13
+ <% end -%>
14
+ </p>
15
+
16
+ <% end -%>
17
+ <%% if action_name != "show" %>
18
+ <%%= link_to "Show this <%= human_name.downcase %>", <%= singular_name %>, class: "rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
19
+ <%%= link_to "Edit this <%= human_name.downcase %>", edit_<%= singular_name %>_path(<%= singular_name %>), class: "rounded-lg py-3 ml-2 px-5 bg-gray-100 inline-block font-medium" %>
20
+ <hr class="mt-6">
21
+ <%% end %>
22
+ </div>
@@ -0,0 +1,15 @@
1
+ <div class="flex mx-auto w-full md:w-2/3">
2
+ <div class="mx-auto">
3
+ <%% if notice.present? %>
4
+ <p class="inline-block py-2 px-3 mb-5 font-medium text-green-500 bg-green-50 rounded-lg" id="notice"><%%= notice %></p>
5
+ <%% end %>
6
+
7
+ <%%= render @<%= singular_table_name %> %>
8
+
9
+ <%%= link_to "Edit this <%= singular_table_name %>", edit_<%= singular_table_name %>_path(@<%= singular_table_name %>), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
10
+ <div class="inline-block ml-2">
11
+ <%%= button_to "Destroy this <%= singular_table_name %>", <%= singular_table_name %>_path(@<%= singular_table_name %>), method: :delete, class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 font-medium" %>
12
+ </div>
13
+ <%%= link_to "Back to <%= plural_table_name %>", <%= index_helper %>_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
14
+ </div>
15
+ </div>
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <browserconfig>
3
+ <msapplication>
4
+ <tile>
5
+ <square150x150logo src="/panda-cms-assets/mstile-150x150.png"/>
6
+ <TileColor>#b91d47</TileColor>
7
+ </tile>
8
+ </msapplication>
9
+ </browserconfig>
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Represents a controller for managing editable content within an iframe.
3
+ * @class
4
+ */
5
+ class EditableController {
6
+ /**
7
+ * Represents the constructor for the Editable class.
8
+ * @param {HTMLIFrameElement} frame - The iFrame element to be used for editing.
9
+ */
10
+ constructor(pageId, frame) {
11
+ this.pageId = pageId;
12
+ this.frame = frame;
13
+ this.frame.style.display = "none";
14
+ this.csrfToken = document.querySelector('meta[name="csrf-token"]').content;
15
+
16
+ this.frame.addEventListener("load", () => {
17
+ this.frameDocument =
18
+ this.frame.contentDocument || this.frame.contentWindow.document;
19
+ this.body = this.frameDocument.body;
20
+ this.head = this.frameDocument.head;
21
+ this.loadEvents();
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Load events for the editable iFrame
27
+ */
28
+ loadEvents() {
29
+ console.debug("[Panda CMS] iFrame loaded...");
30
+ this.embedExternalResources();
31
+ this.styleEditableElements();
32
+ }
33
+
34
+ embedExternalResources() {
35
+ this.addStylesheet(
36
+ this.frameDocument,
37
+ this.head,
38
+ "https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.bubble.css"
39
+ )
40
+ .then(() => {
41
+ return this.loadScript(
42
+ this.frameDocument,
43
+ this.head,
44
+ "https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.js"
45
+ );
46
+ })
47
+ .then(() => {
48
+ return this.loadScript(
49
+ this.frameDocument,
50
+ this.head,
51
+ "https://unpkg.com/quill-image-compress@1.2.11/dist/quill.imageCompressor.min.js"
52
+ );
53
+ })
54
+ .then(() => {
55
+ return this.loadScript(
56
+ this.frameDocument,
57
+ this.head,
58
+ "https://unpkg.com/quill-magic-url@3.0.0/dist/index.js"
59
+ );
60
+ })
61
+ .then(() => {
62
+ return this.loadScript(
63
+ this.frameDocument,
64
+ this.head,
65
+ "https://cdn.jsdelivr.net/npm/quill-markdown-shortcuts@latest/dist/markdownShortcuts.js"
66
+ );
67
+ })
68
+ .then(() => {
69
+ console.debug(
70
+ "[Panda CMS] Dispatching event: pandaCmsExternalResourcesLoaded"
71
+ );
72
+
73
+ // Let the parent know that the external resources have been loaded
74
+ this.frameDocument.dispatchEvent(
75
+ new Event("pandaCmsExternalResourcesLoaded")
76
+ );
77
+
78
+ // Autosave the editable elements
79
+ this.autosaveElements();
80
+
81
+ // This prevents the flash of content before the iFrame is ready
82
+ this.frame.style.display = "";
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Autosave the editable elements in the iFrame
88
+ * @todo #TODO Only do this when we have "autosave" mode enabled?
89
+ */
90
+ autosaveElements() {
91
+ // Grab each element that's editable and append a save handler to it
92
+ var elements = this.frameDocument.querySelectorAll(".ql-editor");
93
+ elements.forEach((element) => {
94
+ element.addEventListener("blur", (event) => {
95
+ var target = event.target;
96
+ var blockContentId = target.parentElement.getAttribute(
97
+ "data-block-content-id"
98
+ );
99
+
100
+ this.bindSaveHandler(blockContentId, target.innerHTML);
101
+ });
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Styles the editable elements in the iFrame
107
+ */
108
+ styleEditableElements() {
109
+ var style = this.frameDocument.createElement("style");
110
+ style.innerHTML = `.ql-container,
111
+ .ql-editor, .ql-editor * {
112
+ margin: inherit !important;
113
+ padding: inherit !important;
114
+ font-size: inherit !important;
115
+ line-height: inherit !important;
116
+ font-family: inherit !important;
117
+ }
118
+
119
+ .ql-toolbar {
120
+ background-color: #282e3270;
121
+ border-radius: 10px;
122
+ }
123
+
124
+ .ql-editor:hover, .ql-container:hover {
125
+ cursor: pointer !important;
126
+ }
127
+
128
+ .ql-editor:hover, .ql-editor:focus, .ql-editor:active {
129
+ background-color: #e1effa;
130
+ cursor: pointer !important;
131
+ font-size: inherit !important;
132
+ -webkit-transition: background-color 500ms linear;
133
+ -ms-transition: background-color 500ms linear;
134
+ transition: background-color 500ms linear;
135
+ }
136
+
137
+ .ql-editor.success {
138
+ background-color: #66bd6a50 !important;
139
+ -webkit-transition: background-color 1000ms linear;
140
+ -ms-transition: background-color 1000ms linear;
141
+ transition: background-color 1000ms linear;
142
+ }
143
+
144
+ .ql-tooltip {
145
+ z-index: 999;
146
+ min-width: 90vw;
147
+ }`;
148
+
149
+ this.head.append(style);
150
+ }
151
+
152
+ bindSaveHandler(blockContentId, content) {
153
+ console.debug(`[Panda CMS] Calling save handler for ${blockContentId}...`);
154
+ fetch(`/admin/pages/${this.pageId}/block_contents/${blockContentId}`, {
155
+ method: "PATCH",
156
+ headers: {
157
+ "Content-Type": "application/json",
158
+ "X-CSRF-Token": this.csrfToken,
159
+ },
160
+ body: JSON.stringify({ content: content }),
161
+ })
162
+ .then((response) => response.json())
163
+ .then((data) => {
164
+ var editableBlock = this.frameDocument.querySelector(
165
+ `[data-block-content-id="${blockContentId}"] .ql-editor`
166
+ );
167
+ editableBlock.classList.add("success");
168
+ setTimeout(() => {
169
+ editableBlock.classList.remove("success");
170
+ }, 1500);
171
+ })
172
+ .catch((error) => {
173
+ console.log(error);
174
+ alert("Error updating. Please contact the support team!", error);
175
+ });
176
+ }
177
+
178
+ addStylesheet(frameDocument, head, href) {
179
+ return new Promise(function (resolve, reject) {
180
+ let link = frameDocument.createElement("link");
181
+ link.rel = "stylesheet";
182
+ link.href = href;
183
+ link.media = "none";
184
+ head.append(link);
185
+
186
+ link.onload = () => {
187
+ if (link.media != "all") {
188
+ link.media = "all";
189
+ }
190
+ console.debug(`[Panda CMS] Stylesheet loaded: ${href}`);
191
+ resolve(link);
192
+ };
193
+ link.onerror = () =>
194
+ reject(new Error(`[Panda CMS] Stylesheet load error for ${href}`));
195
+ });
196
+ }
197
+
198
+ loadScript(frameDocument, head, src) {
199
+ return new Promise(function (resolve, reject) {
200
+ let script = frameDocument.createElement("script");
201
+ script.src = src;
202
+ head.append(script);
203
+
204
+ script.onload = () => {
205
+ console.debug(`[Panda CMS] Script loaded: ${src}`);
206
+ resolve(script);
207
+ };
208
+ script.onerror = () =>
209
+ reject(new Error(`[Panda CMS] Script load error for ${src}`));
210
+ });
211
+ }
212
+ }
Binary file