panda-cms 0.7.3 → 0.7.4

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  3. data/app/assets/builds/panda.cms.css +2 -6
  4. data/app/assets/tailwind/application.css +178 -0
  5. data/app/assets/tailwind/tailwind.config.js +15 -0
  6. data/app/builders/panda/cms/form_builder.rb +15 -32
  7. data/app/components/panda/cms/admin/flash_message_component.html.erb +2 -2
  8. data/app/components/panda/cms/admin/user_activity_component.rb +7 -18
  9. data/app/controllers/panda/cms/admin/block_contents_controller.rb +0 -1
  10. data/app/controllers/panda/cms/admin/forms_controller.rb +0 -1
  11. data/app/controllers/panda/cms/admin/my_profile_controller.rb +43 -0
  12. data/app/controllers/panda/cms/admin/pages_controller.rb +14 -4
  13. data/app/controllers/panda/cms/admin/posts_controller.rb +7 -22
  14. data/app/controllers/panda/cms/form_submissions_controller.rb +2 -0
  15. data/app/controllers/panda/cms/pages_controller.rb +10 -8
  16. data/app/javascript/panda/cms/controllers/index.js +4 -2
  17. data/app/javascript/panda/cms/controllers/slug_controller.js +64 -31
  18. data/app/javascript/panda/cms/controllers/theme_form_controller.js +9 -0
  19. data/app/jobs/panda/cms/record_visit_job.rb +12 -14
  20. data/app/models/panda/cms/application_record.rb +1 -0
  21. data/app/models/panda/cms/block.rb +9 -17
  22. data/app/models/panda/cms/block_content.rb +5 -6
  23. data/app/models/panda/cms/page.rb +24 -11
  24. data/app/models/panda/cms/post.rb +3 -5
  25. data/app/models/panda/cms/redirect.rb +5 -0
  26. data/app/models/panda/cms/template.rb +6 -7
  27. data/app/models/panda/cms/visit.rb +1 -1
  28. data/app/models/panda/social/instagram_post.rb +15 -0
  29. data/app/services/panda/cms/html_to_editor_js_converter.rb +6 -13
  30. data/app/services/panda/social/instagram_feed_service.rb +61 -0
  31. data/app/views/panda/cms/admin/my_profile/edit.html.erb +35 -0
  32. data/app/views/panda/cms/admin/pages/index.html.erb +1 -1
  33. data/app/views/panda/cms/admin/pages/new.html.erb +3 -3
  34. data/app/views/panda/cms/admin/posts/_form.html.erb +10 -0
  35. data/app/views/panda/cms/admin/posts/edit.html.erb +3 -2
  36. data/app/views/panda/cms/admin/posts/index.html.erb +1 -1
  37. data/app/views/panda/cms/admin/settings/index.html.erb +2 -0
  38. data/app/views/panda/cms/admin/shared/_sidebar.html.erb +1 -1
  39. data/app/views/panda/cms/shared/_header.html.erb +4 -2
  40. data/app/views/panda/cms/shared/_importmap.html.erb +1 -0
  41. data/config/locales/en.yml +5 -0
  42. data/config/routes.rb +3 -0
  43. data/db/migrate/20250120235542_remove_paper_trail.rb +55 -0
  44. data/db/migrate/20250126234001_create_panda_social_instagram_posts.rb +14 -0
  45. data/db/migrate/20250504221812_add_current_theme_to_panda_cms_users.rb +5 -0
  46. data/lib/panda/cms/demo_site_generator.rb +25 -4
  47. data/lib/panda/cms/editor_js_content.rb +21 -0
  48. data/lib/panda/cms/engine.rb +7 -5
  49. data/lib/panda-cms/version.rb +1 -1
  50. data/lib/panda-cms.rb +13 -0
  51. data/lib/tasks/panda/cms/install.rake +23 -0
  52. data/lib/tasks/panda/social/instagram.rake +18 -0
  53. data/public/panda-cms-assets/editor-js/core/editorjs.min.js +83 -0
  54. data/public/panda-cms-assets/editor-js/plugins/embed.min.js +2 -0
  55. data/public/panda-cms-assets/editor-js/plugins/header.min.js +9 -0
  56. data/public/panda-cms-assets/editor-js/plugins/nested-list.min.js +2 -0
  57. data/public/panda-cms-assets/editor-js/plugins/paragraph.min.js +9 -0
  58. data/public/panda-cms-assets/editor-js/plugins/quote.min.js +2 -0
  59. data/public/panda-cms-assets/editor-js/plugins/simple-image.min.js +2 -0
  60. data/public/panda-cms-assets/editor-js/plugins/table.min.js +2 -0
  61. metadata +36 -557
  62. data/app/models/action_text/rich_text_version.rb +0 -6
  63. data/app/models/panda/cms/block_content_version.rb +0 -8
  64. data/app/models/panda/cms/page_version.rb +0 -8
  65. data/app/models/panda/cms/post_version.rb +0 -8
  66. data/app/models/panda/cms/template_version.rb +0 -8
  67. data/app/models/panda/cms/version.rb +0 -8
  68. data/config/initializers/panda/cms/paper_trail.rb +0 -7
  69. data/db/migrate/20240904200605_create_action_text_tables.action_text.rb +0 -24
  70. data/db/migrate/20241119214549_remove_action_text_from_posts.rb +0 -9
@@ -14,57 +14,85 @@ export default class extends Controller {
14
14
 
15
15
  connect() {
16
16
  console.debug("[Panda CMS] Slug handler connected...");
17
- // Generate path on initial load if title exists
18
- if (this.input_textTarget.value) {
19
- this.generatePath();
20
- }
17
+ // Don't auto-generate on connect anymore
21
18
  }
22
19
 
23
- generatePath() {
24
- const title = this.input_textTarget.value;
25
- if (!title) return;
20
+ generatePath(event) {
21
+ // Prevent event object from being used as input
22
+ const title = this.input_textTarget.value.trim();
23
+ console.debug("[Panda CMS] Generating path from title:", title);
26
24
 
27
- // Convert title to slug format
28
- const slug = title
29
- .toLowerCase()
30
- .replace(/[^a-z0-9]+/g, "-")
31
- .replace(/^-+|-+$/g, "");
25
+ if (!title) {
26
+ this.output_textTarget.value = "";
27
+ return;
28
+ }
29
+
30
+ // Only generate path if output is empty OR user has not manually edited it
31
+ if (!this.output_textTarget.value || !this.output_textTarget.dataset.userEdited) {
32
+ // Convert title to slug format
33
+ const slug = this.createSlug(title);
34
+ console.debug("[Panda CMS] Generated slug:", slug);
32
35
 
33
- // Only add year/month prefix for posts
34
- if (this.addDatePrefixValue) {
35
- // Get current date for year/month
36
- const now = new Date();
37
- const year = now.getFullYear();
38
- const month = String(now.getMonth() + 1).padStart(2, "0");
39
-
40
- // Add leading slash and use date format
41
- this.output_textTarget.value = `/${year}/${month}/${slug}`;
42
- } else {
43
- // Add leading slash for regular pages
44
- this.output_textTarget.value = `/${slug}`;
36
+ // Only add year/month prefix for posts
37
+ if (this.addDatePrefixValue) {
38
+ // Get current date for year/month
39
+ const now = new Date();
40
+ const year = now.getFullYear();
41
+ const month = String(now.getMonth() + 1).padStart(2, "0");
42
+
43
+ // Add leading slash and use date format
44
+ this.output_textTarget.value = `/${year}/${month}/${slug}`;
45
+ } else {
46
+ // If we have a parent selected, let setPrePath handle it
47
+ if (this.input_selectTarget.value) {
48
+ this.setPrePath(slug);
49
+ } else {
50
+ // Add leading slash for regular pages
51
+ this.output_textTarget.value = `/${slug}`;
52
+ }
53
+ }
54
+ console.debug("[Panda CMS] Final path value:", this.output_textTarget.value);
45
55
  }
46
56
  }
47
57
 
48
- setPrePath() {
58
+ setPrePath(slug = null) {
49
59
  try {
60
+ // Don't do anything if we're passed the event object
61
+ if (slug && typeof slug === 'object') {
62
+ slug = null;
63
+ }
64
+
50
65
  const match = this.input_selectTarget.options[this.input_selectTarget.selectedIndex].text.match(/.*\((.*)\)$/);
51
66
  if (match) {
52
- this.parent_slugs = match[1];
53
- const prePath = (this.existing_rootTarget.value + this.parent_slugs).replace(/\/$/, "");
54
- // Ensure we don't double up slashes
55
- const currentPath = this.output_textTarget.value.replace(/^\//, "");
56
- this.output_textTarget.value = `${prePath}/${currentPath}`;
67
+ const parentPath = match[1].replace(/\/$/, ""); // Remove trailing slash if present
68
+
69
+ // If we have a specific slug passed in, use it
70
+ // Otherwise only use the title-based slug if we have a title
71
+ const currentSlug = slug ||
72
+ (this.input_textTarget.value.trim() ? this.createSlug(this.input_textTarget.value.trim()) : "");
73
+
74
+ // Set the full path including parent path
75
+ this.output_textTarget.value = currentSlug
76
+ ? `${parentPath}/${currentSlug}`
77
+ : `${parentPath}/`;
78
+
79
+ console.debug("[Panda CMS] Set path with parent:", this.output_textTarget.value);
57
80
  }
58
81
  } catch (e) {
59
82
  console.error("[Panda CMS] Error setting pre-path:", e);
83
+ // Clear the output on error
84
+ this.output_textTarget.value = "";
60
85
  }
61
86
  }
62
87
 
63
88
  createSlug(input) {
64
- return input
89
+ if (!input || typeof input !== 'string') return "";
90
+ const slug = input
65
91
  .toLowerCase()
92
+ .trim()
66
93
  .replace(/[^a-z0-9]+/g, "-")
67
94
  .replace(/^-+|-+$/g, "");
95
+ return slug;
68
96
  }
69
97
 
70
98
  trimStartEnd(str, ch) {
@@ -74,4 +102,9 @@ export default class extends Controller {
74
102
  while (end > start && str[end - 1] === ch) --end;
75
103
  return start > 0 || end < str.length ? str.substring(start, end) : str;
76
104
  }
105
+
106
+ // Add handler for manual path edits
107
+ handlePathInput() {
108
+ this.output_textTarget.dataset.userEdited = "true";
109
+ }
77
110
  }
@@ -0,0 +1,9 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ // Connects to data-controller="theme-form"
4
+ export default class extends Controller {
5
+ updateTheme(event) {
6
+ const newTheme = event.target.value;
7
+ document.documentElement.dataset.theme = newTheme;
8
+ }
9
+ }
@@ -4,26 +4,24 @@ module Panda
4
4
  queue_as :default
5
5
 
6
6
  def perform(
7
- url: nil,
7
+ path:,
8
+ user_id: nil,
9
+ redirect_id: nil,
10
+ page_id: nil,
8
11
  user_agent: nil,
9
- referrer: nil,
10
12
  ip_address: nil,
11
- page_id: nil,
12
- current_user_id: nil,
13
- params: [],
14
- visited_at: nil,
15
- redirect_id: nil
13
+ referer: nil,
14
+ params: []
16
15
  )
17
16
  Panda::CMS::Visit.create!(
18
- url: url,
17
+ url: path,
18
+ user_id: user_id,
19
+ redirect_id: redirect_id,
20
+ page_id: page_id,
19
21
  user_agent: user_agent,
20
- referrer: referrer,
21
22
  ip_address: ip_address,
22
- page_id: page_id,
23
- redirect_id: redirect_id,
24
- user_id: current_user_id,
25
- params: params,
26
- visited_at: visited_at
23
+ referrer: referer, # TODO: Fix the naming of this column
24
+ params: params
27
25
  )
28
26
  end
29
27
  end
@@ -2,6 +2,7 @@ module Panda
2
2
  module CMS
3
3
  class ApplicationRecord < ActiveRecord::Base
4
4
  self.abstract_class = true
5
+ self.implicit_order_column = "created_at"
5
6
  end
6
7
  end
7
8
  end
@@ -3,31 +3,23 @@ module Panda
3
3
  class Block < ApplicationRecord
4
4
  self.table_name = "panda_cms_blocks"
5
5
 
6
- belongs_to :template, foreign_key: :panda_cms_template_id, class_name: "Panda::CMS::Template", inverse_of: :blocks, optional: true
7
- has_many :block_contents, foreign_key: :panda_cms_block_id, class_name: "Panda::CMS::BlockContent", inverse_of: :block
6
+ belongs_to :template, foreign_key: :panda_cms_template_id, class_name: "Panda::CMS::Template"
7
+ has_many :block_contents, foreign_key: :panda_cms_block_id, class_name: "Panda::CMS::BlockContent", dependent: :destroy
8
8
 
9
- validates :kind, presence: true
10
9
  validates :name, presence: true
11
10
  validates :key, presence: true, uniqueness: {scope: :panda_cms_template_id, case_sensitive: false}
11
+ validates :kind, presence: true
12
12
 
13
- # Validation for presence on template intentionally skipped to allow global elements
14
-
15
- # NB: Commented out values are not yet implemented
16
13
  enum :kind, {
17
14
  plain_text: "plain_text",
18
15
  rich_text: "rich_text",
16
+ image: "image",
17
+ video: "video",
18
+ audio: "audio",
19
+ file: "file",
20
+ code: "code",
19
21
  iframe: "iframe",
20
- list: "list",
21
- code: "code"
22
- # image: "image",
23
- # video: "video",
24
- # audio: "audio",
25
- # file: "file",
26
- # iframe: "iframe",
27
- # quote: "quote",
28
- # list: "list"
29
- # table: "table",
30
- # form: "form"
22
+ quote: "quote"
31
23
  }
32
24
  end
33
25
  end
@@ -5,14 +5,13 @@ module Panda
5
5
 
6
6
  self.table_name = "panda_cms_block_contents"
7
7
 
8
- has_paper_trail versions: {
9
- class_name: "Panda::CMS::BlockContentVersion"
10
- }
11
-
12
- belongs_to :page, foreign_key: :panda_cms_page_id, class_name: "Panda::CMS::Page", inverse_of: :block_contents, optional: true, touch: true
13
- belongs_to :block, foreign_key: :panda_cms_block_id, class_name: "Panda::CMS::Block", inverse_of: :block_contents, optional: false
8
+ belongs_to :page, foreign_key: :panda_cms_page_id, class_name: "Panda::CMS::Page", touch: true
9
+ belongs_to :block, foreign_key: :panda_cms_block_id, class_name: "Panda::CMS::Block"
14
10
 
15
11
  validates :block, presence: true, uniqueness: {scope: :page}
12
+
13
+ store_accessor :content, [], prefix: true
14
+ store_accessor :cached_content, [], prefix: true
16
15
  end
17
16
  end
18
17
  end
@@ -7,15 +7,9 @@ module Panda
7
7
  self.table_name = "panda_cms_pages"
8
8
  self.implicit_order_column = "lft"
9
9
 
10
- has_paper_trail versions: {
11
- class_name: "Panda::CMS::PageVersion"
12
- }
13
-
14
- after_save :after_save
15
-
16
- belongs_to :template, foreign_key: :panda_cms_template_id, class_name: "Panda::CMS::Template", inverse_of: :pages, optional: false, counter_cache: :pages_count
17
- has_many :blocks, through: :template
18
- has_many :block_contents, foreign_key: :panda_cms_page_id, class_name: "Panda::CMS::BlockContent", inverse_of: :page
10
+ belongs_to :template, class_name: "Panda::CMS::Template", foreign_key: :panda_cms_template_id
11
+ has_many :block_contents, class_name: "Panda::CMS::BlockContent", foreign_key: :panda_cms_page_id, dependent: :destroy
12
+ has_many :blocks, through: :block_contents
19
13
  has_many :menu_items, foreign_key: :panda_cms_page_id, class_name: "Panda::CMS::MenuItem", inverse_of: :page
20
14
  has_many :menus, through: :menu_items
21
15
  has_many :menus_of_parent, through: :parent, source: :menus
@@ -25,9 +19,10 @@ module Panda
25
19
 
26
20
  validates :path,
27
21
  presence: true,
28
- uniqueness: true,
29
22
  format: {with: /\A\/.*\z/, message: "must start with a forward slash"}
30
23
 
24
+ validate :validate_unique_path_in_scope
25
+
31
26
  validates :parent,
32
27
  presence: true,
33
28
  unless: -> { path == "/" }
@@ -44,6 +39,9 @@ module Panda
44
39
  archived: "archived"
45
40
  }
46
41
 
42
+ # Callbacks
43
+ after_save :handle_after_save
44
+
47
45
  #
48
46
  # Update any menus which include this page or its parent as a menu item
49
47
  #
@@ -57,13 +55,28 @@ module Panda
57
55
 
58
56
  private
59
57
 
58
+ def validate_unique_path_in_scope
59
+ # Skip validation if path is not present (other validations will catch this)
60
+ return if path.blank?
61
+
62
+ # Find any other pages with the same path
63
+ other_page = self.class.where(path: path).where.not(id: id).first
64
+
65
+ if other_page
66
+ # If there's another page with the same path, check if it has a different parent
67
+ if other_page.parent_id == parent_id
68
+ errors.add(:path, "has already been taken in this section")
69
+ end
70
+ end
71
+ end
72
+
60
73
  #
61
74
  # After save callbacks
62
75
  #
63
76
  # @return nil
64
77
  # @visibility private
65
78
  #
66
- def after_save
79
+ def handle_after_save
67
80
  generate_content_blocks
68
81
  update_existing_menu_items
69
82
  update_auto_menus
@@ -10,12 +10,10 @@ module Panda
10
10
 
11
11
  self.table_name = "panda_cms_posts"
12
12
 
13
- has_paper_trail versions: {
14
- class_name: "Panda::CMS::PostVersion"
15
- }
16
-
17
13
  belongs_to :user, class_name: "Panda::CMS::User"
18
- belongs_to :author, class_name: "Panda::CMS::User"
14
+ belongs_to :author, class_name: "Panda::CMS::User", optional: true
15
+ has_many :block_contents, as: :blockable, dependent: :destroy
16
+ has_many :blocks, through: :block_contents
19
17
 
20
18
  validates :title, presence: true
21
19
  validates :slug,
@@ -6,6 +6,11 @@ module Panda
6
6
 
7
7
  validates :status_code, presence: true
8
8
  validates :visits, presence: true
9
+ validates :origin_path, presence: true
10
+ validates :destination_path, presence: true
11
+
12
+ validates :origin_path, format: {with: /\A\/.*\z/, message: "must start with a forward slash"}
13
+ validates :destination_path, format: {with: /\A\/.*\z/, message: "must start with a forward slash"}
9
14
  end
10
15
  end
11
16
  end
@@ -4,13 +4,8 @@ module Panda
4
4
  class Template < ApplicationRecord
5
5
  self.table_name = "panda_cms_templates"
6
6
 
7
- # Enables versioning for the Template model using the `has_paper_trail` gem.
8
- has_paper_trail versions: {
9
- class_name: "Panda::CMS::TemplateVersion"
10
- }
11
-
12
7
  # Associations
13
- has_many :pages, class_name: "Panda::CMS::Page", dependent: :restrict_with_error, inverse_of: :template, foreign_key: :panda_cms_template_id
8
+ has_many :pages, class_name: "Panda::CMS::Page", dependent: :restrict_with_error, inverse_of: :template, foreign_key: :panda_cms_template_id, counter_cache: :pages_count
14
9
  has_many :blocks, class_name: "Panda::CMS::Block", dependent: :restrict_with_error, inverse_of: :template, foreign_key: :panda_cms_template_id
15
10
  has_many :block_contents, through: :blocks
16
11
 
@@ -26,13 +21,17 @@ module Panda
26
21
 
27
22
  # Scopes
28
23
  scope :available, -> {
29
- where("max_uses IS NULL OR (pages_count IS NOT NULL AND pages_count < max_uses)")
24
+ where("max_uses IS NULL OR (max_uses > 0 AND pages_count < max_uses)")
30
25
  }
31
26
 
32
27
  def self.default
33
28
  find_by(file_path: "layouts/page")
34
29
  end
35
30
 
31
+ def self.reset_counter_cache
32
+ find_each { |template| template.update_column(:pages_count, template.pages.count) }
33
+ end
34
+
36
35
  # Generate missing blocks for all templates
37
36
  # @return [void]
38
37
  def self.generate_missing_blocks
@@ -1,7 +1,7 @@
1
1
  module Panda
2
2
  module CMS
3
3
  class Visit < ApplicationRecord
4
- belongs_to :page, class_name: "Panda::CMS::Page", foreign_key: :page_id, optional: true
4
+ belongs_to :page, class_name: "Panda::CMS::Page", foreign_key: :panda_cms_page_id, optional: true
5
5
  belongs_to :user, class_name: "Panda::CMS::User", foreign_key: :user_id, optional: true
6
6
  belongs_to :redirect, class_name: "Panda::CMS::Redirect", foreign_key: :redirect_id, optional: true
7
7
  end
@@ -0,0 +1,15 @@
1
+ module Panda
2
+ module Social
3
+ class InstagramPost < ApplicationRecord
4
+ self.table_name = "panda_social_instagram_posts"
5
+
6
+ has_one_attached :image
7
+
8
+ validates :instagram_id, presence: true, uniqueness: true
9
+ validates :caption, presence: true
10
+ validates :posted_at, presence: true
11
+
12
+ scope :ordered, -> { order(posted_at: :desc) }
13
+ end
14
+ end
15
+ end
@@ -131,26 +131,19 @@ module Panda
131
131
  end
132
132
  end
133
133
 
134
- # Add any remaining text as a final paragraph
135
- if current_text.present?
136
- # Split any remaining text on double line breaks
137
- paragraphs = current_text.split(/\n\n+/).map(&:strip)
138
- paragraphs.each do |paragraph|
139
- blocks << create_paragraph_block(paragraph) if paragraph.present?
140
- end
141
- end
142
-
143
- raise ConversionError, "No valid content blocks found" if blocks.empty?
134
+ # Add any remaining text
135
+ blocks << create_paragraph_block(current_text) if current_text.present?
144
136
 
137
+ # Return the complete EditorJS structure
145
138
  {
146
139
  "time" => Time.current.to_i * 1000,
147
140
  "blocks" => blocks,
148
141
  "version" => "2.28.2"
149
142
  }
150
- rescue Nokogiri::SyntaxError => e
151
- raise ConversionError, "Invalid HTML syntax: #{e.message}"
152
143
  rescue => e
153
- raise ConversionError, "Conversion failed: #{e.message}"
144
+ Rails.logger.error "HTML to EditorJS conversion failed: #{e.message}"
145
+ Rails.logger.error e.backtrace.join("\n")
146
+ raise ConversionError, "Failed to convert HTML to EditorJS format: #{e.message}"
154
147
  end
155
148
  end
156
149
 
@@ -0,0 +1,61 @@
1
+ require "http"
2
+ require "down"
3
+
4
+ module Panda
5
+ module Social
6
+ class InstagramFeedService
7
+ GRAPH_API_VERSION = "v19.0"
8
+ GRAPH_API_BASE_URL = "https://graph.instagram.com/#{GRAPH_API_VERSION}"
9
+
10
+ def initialize(access_token)
11
+ @access_token = access_token
12
+ end
13
+
14
+ def sync_recent_posts
15
+ fetch_media.each do |post_data|
16
+ process_post(post_data)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def fetch_media
23
+ response = HTTP.get("#{GRAPH_API_BASE_URL}/me/media", params: {
24
+ access_token: @access_token,
25
+ fields: "id,caption,media_type,media_url,permalink,timestamp"
26
+ })
27
+
28
+ return [] unless response.status.success?
29
+
30
+ JSON.parse(response.body.to_s)["data"]
31
+ end
32
+
33
+ def process_post(post_data)
34
+ return unless post_data["media_type"] == "IMAGE"
35
+
36
+ instagram_post = InstagramPost.find_or_initialize_by(instagram_id: post_data["id"])
37
+
38
+ instagram_post.assign_attributes(
39
+ caption: post_data["caption"],
40
+ posted_at: Time.zone.parse(post_data["timestamp"]),
41
+ permalink: post_data["permalink"]
42
+ )
43
+
44
+ if instagram_post.new_record? || instagram_post.changed?
45
+ # Download and attach image
46
+ tempfile = Down.download(post_data["media_url"])
47
+ instagram_post.image.attach(
48
+ io: tempfile,
49
+ filename: File.basename(post_data["media_url"])
50
+ )
51
+
52
+ instagram_post.save!
53
+ end
54
+ rescue Down::Error => e
55
+ Rails.logger.error "Failed to download Instagram image: #{e.message}"
56
+ rescue => e
57
+ Rails.logger.error "Error processing Instagram post #{post_data["id"]}: #{e.message}"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,35 @@
1
+ <%= render Panda::CMS::Admin::ContainerComponent.new do |component| %>
2
+ <% component.with_heading(text: "My Profile", level: 1) %>
3
+
4
+ <%= panda_cms_form_with model: user,
5
+ url: admin_my_profile_path,
6
+ method: :patch,
7
+ data: { controller: "theme-form" } do |f| %>
8
+ <% if user.errors.any? %>
9
+ <div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-md">
10
+ <div class="text-sm text-red-600">
11
+ <% user.errors.full_messages.each do |message| %>
12
+ <p><%= message %></p>
13
+ <% end %>
14
+ </div>
15
+ </div>
16
+ <% end %>
17
+
18
+ <div class="space-y-4">
19
+ <%= f.text_field :firstname %>
20
+
21
+ <%= f.text_field :lastname %>
22
+
23
+ <%= f.email_field :email %>
24
+
25
+ <%= f.select :current_theme,
26
+ [["Default", "default"], ["Sky", "sky"]],
27
+ { label: "Theme" },
28
+ data: { action: "change->theme-form#updateTheme" } %>
29
+ </div>
30
+
31
+ <%= f.submit "Update Profile",
32
+ class: "btn btn-primary mt-6",
33
+ data: { disable_with: "Saving..." } %>
34
+ <% end %>
35
+ <% end %>
@@ -12,7 +12,7 @@
12
12
  </div>
13
13
  <% end %>
14
14
  <% table.column("Status") { |page| render Panda::CMS::Admin::TagComponent.new(status: page.status) } %>
15
- <% table.column("Last Updated") { |page| render Panda::CMS::Admin::UserActivityComponent.new(whodunnit_to: page)} %>
15
+ <% table.column("Last Updated") { |page| render Panda::CMS::Admin::UserActivityComponent.new(model: page) } %>
16
16
  <% end %>
17
17
  <% else %>
18
18
  <div class="p-6 bg-error/10 text-error rounded-lg">
@@ -6,9 +6,9 @@
6
6
  <div data-controller="slug">
7
7
  <input type="hidden" value="<%= Panda::CMS::Current.root %>" data-slug-target="existing_root">
8
8
  <%= f.select :parent_id, options, {}, { "data-slug-target": "input_select", "data-action": "change->slug#setPrePath" } %>
9
- <%= f.text_field :title, { data: { "slug-target": "input_text", action: "focusout->slug#generatePath" } } %>
10
- <%= f.text_field :path, { meta: t(".path.meta"), data: { prefix: Panda::CMS::Current.root, "slug-target": "output_text" } } %>
11
- <%= f.collection_select :panda_cms_template_id, Panda::CMS::Template.available, :id, :name %>
9
+ <%= f.text_field :title, { data: { "slug-target": "input_text", action: "input->slug#generatePath focusout->slug#generatePath" } } %>
10
+ <%= f.text_field :path, { meta: t(".path.meta"), data: { prefix: Panda::CMS::Current.root, "slug-target": "output_text", action: "input->slug#handlePathInput" } } %>
11
+ <%= f.collection_select :panda_cms_template_id, available_templates, :id, :name %>
12
12
  <%= f.button "Create Page" %>
13
13
  </div>
14
14
  <% end %>
@@ -1,4 +1,14 @@
1
1
  <%= panda_cms_form_with model: post, url: url, method: post.persisted? ? :put : :post do |f| %>
2
+ <% if post.errors.any? %>
3
+ <div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-md">
4
+ <div class="text-sm text-red-600">
5
+ <% post.errors.full_messages.each do |message| %>
6
+ <p><%= message %></p>
7
+ <% end %>
8
+ </div>
9
+ </div>
10
+ <% end %>
11
+
2
12
  <div data-controller="slug" data-slug-add-date-prefix-value="true">
3
13
  <%= f.text_field :title,
4
14
  class: "block w-full rounded-md border-0 p-2 text-gray-900 ring-1 ring-inset ring-mid placeholder:text-gray-300 focus:ring-1 focus:ring-inset focus:ring-dark sm:leading-6 hover:pointer",
@@ -1,6 +1,7 @@
1
1
  <%= render Panda::CMS::Admin::ContainerComponent.new do |component| %>
2
- <% component.with_heading(text: "Edit Post", level: 1) do |heading| %>
2
+ <% component.with_heading(text: post.title, level: 1) do |heading| %>
3
+ <% heading.with_button(action: :view, text: "View Post", link: post_path(post.admin_param)) %>
3
4
  <% end %>
4
5
 
5
- <%= render "form", post: post, url: url, method: :patch %>
6
+ <%= render "form", post: post, url: admin_post_path(post.admin_param), method: :patch %>
6
7
  <% end %>
@@ -14,7 +14,7 @@
14
14
  <% end %>
15
15
  <% table.column("Status") { |post| render Panda::CMS::Admin::TagComponent.new(status: post.status) } %>
16
16
  <% table.column("Published") { |post| render Panda::CMS::Admin::UserActivityComponent.new(at: post.published_at, user: post.author)} %>
17
- <% table.column("Last Updated") { |post| render Panda::CMS::Admin::UserActivityComponent.new(whodunnit_to: post)} %>
17
+ <% table.column("Last Updated") { |post| render Panda::CMS::Admin::UserActivityComponent.new(at: post.updated_at)} %>
18
18
  <% end %>
19
19
 
20
20
  <% end %>
@@ -12,6 +12,8 @@
12
12
 
13
13
  <%= render Panda::CMS::Admin::PanelComponent.new do |panel| %>
14
14
  <% panel.with_heading(text: "Integrations") %>
15
+
16
+ <p class="text-base leading-loose"><i class="mr-2 text-active fa-brands fa-instagram"></i> <span class="font-medium">Instagram:</span> <%= Panda::CMS.config.instagram[:enabled] ? "Connected (@#{Panda::CMS.config.instagram[:username]})" : "Not Connected" %></p>
15
17
  <% end %>
16
18
 
17
19
  <div class="text-center mt-6 space-y-2">
@@ -23,7 +23,7 @@
23
23
  <% end %>
24
24
  </li>
25
25
  <li class="mt-auto">
26
- <%= button_to '#', data: { turbo: false }, class: nav_highlight_colour_classes(request) + " w-full" do %>
26
+ <%= link_to edit_admin_my_profile_path, class: nav_highlight_colour_classes(request) + " w-full", title: "Edit my Profile" do %>
27
27
  <% if !current_user.image_url.to_s.empty? %>
28
28
  <span class="text-center"><img src="<%= current_user.image_url %>" class="w-auto h-7 rounded-full"></span>
29
29
  <% else %>
@@ -1,5 +1,7 @@
1
1
  <!DOCTYPE html>
2
- <html data-theme="default" class="<%= html_class ||= "" %>">
2
+ <html
3
+ data-theme="<%= Panda::CMS::Current&.user&.current_theme || "default" %>"
4
+ class="<%= html_class ||= "" %>">
3
5
  <head>
4
6
  <title><%= title_tag %></title>
5
7
  <%= csrf_meta_tags %>
@@ -11,5 +13,5 @@
11
13
  <%= render "panda/cms/shared/favicons" %>
12
14
  <%= yield :head %>
13
15
  </head>
14
- <body class="overflow-hidden h-full">
16
+ <body class="overflow-hidden h-full" data-environment="<%= Rails.env %>">
15
17
  <%= render "panda/cms/admin/shared/flash" %>
@@ -22,6 +22,7 @@
22
22
  "panda/cms/controllers/slug_controller": asset_path("panda/cms/controllers/slug_controller.js"),
23
23
  "panda/cms/controllers/editor_form_controller": asset_path("panda/cms/controllers/editor_form_controller.js"),
24
24
  "panda/cms/controllers/editor_iframe_controller": asset_path("panda/cms/controllers/editor_iframe_controller.js"),
25
+ "panda/cms/controllers/theme_form_controller": asset_path("panda/cms/controllers/theme_form_controller.js"),
25
26
  # Main controller loader
26
27
  "controllers": asset_path("panda/cms/controllers/index.js"),
27
28
  }