panda-cms 0.7.0 → 0.7.2

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/panda.cms.css +0 -50
  3. data/app/components/panda/cms/admin/table_component.html.erb +4 -1
  4. data/app/components/panda/cms/code_component.rb +2 -1
  5. data/app/components/panda/cms/rich_text_component.html.erb +86 -2
  6. data/app/components/panda/cms/rich_text_component.rb +131 -20
  7. data/app/controllers/panda/cms/admin/block_contents_controller.rb +18 -7
  8. data/app/controllers/panda/cms/admin/files_controller.rb +22 -12
  9. data/app/controllers/panda/cms/admin/posts_controller.rb +33 -11
  10. data/app/controllers/panda/cms/pages_controller.rb +29 -0
  11. data/app/controllers/panda/cms/posts_controller.rb +26 -4
  12. data/app/helpers/panda/cms/admin/posts_helper.rb +23 -32
  13. data/app/helpers/panda/cms/posts_helper.rb +32 -0
  14. data/app/javascript/panda/cms/controllers/dashboard_controller.js +0 -1
  15. data/app/javascript/panda/cms/controllers/editor_form_controller.js +134 -11
  16. data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +395 -130
  17. data/app/javascript/panda/cms/controllers/slug_controller.js +33 -43
  18. data/app/javascript/panda/cms/editor/editor_js_config.js +202 -73
  19. data/app/javascript/panda/cms/editor/editor_js_initializer.js +243 -194
  20. data/app/javascript/panda/cms/editor/plain_text_editor.js +1 -1
  21. data/app/javascript/panda/cms/editor/resource_loader.js +89 -0
  22. data/app/javascript/panda/cms/editor/rich_text_editor.js +162 -0
  23. data/app/models/panda/cms/page.rb +18 -0
  24. data/app/models/panda/cms/post.rb +61 -3
  25. data/app/models/panda/cms/redirect.rb +2 -2
  26. data/app/views/panda/cms/admin/posts/_form.html.erb +15 -4
  27. data/app/views/panda/cms/admin/posts/index.html.erb +5 -3
  28. data/config/routes.rb +34 -6
  29. data/db/migrate/20250106223303_add_author_id_to_panda_cms_posts.rb +5 -0
  30. data/lib/panda/cms/editor_js_content.rb +14 -1
  31. data/lib/panda/cms/engine.rb +4 -0
  32. data/lib/panda-cms/version.rb +1 -1
  33. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c002856b3c2b19f78b45a5fd2bb58287bd8b49cfc305f4e400c52e41c38fa9b
4
- data.tar.gz: df33fb1880dab8f2361d36859a2c0a556710d2c5879f3a0ae052b4ea7a4e0d78
3
+ metadata.gz: 74ac7b8103cc0535b6bbfce15e834b7b7e6dfe033fc168ca5d31bb8615b02ede
4
+ data.tar.gz: e9c66a7a385abdbb7a6c52418c754b93d8a2fcc8afaf337fc69379c8a4c2e365
5
5
  SHA512:
6
- metadata.gz: db39a72f704c8aebd0b1049b6efba4c41ab5fb09dc8320de6693848829ef2b4ce3e4a90d081a38ce5a44ec6a7e21cae4edf1ce122a678d2d1e4aa85be28de757
7
- data.tar.gz: 1b3260d6ca7bffbfa794894f90f5db94fb71a47510bf3eeef599264883baee32d52e1527e06492d372c30f790920d6688d02644d2d186cfe59c9bbd380fad431
6
+ metadata.gz: fdececd7daec565b2cb6d2ba5a0ca27c84b518a0b1178ca664c848b9078f45ca4045d749a3816e5f0cb2bbeef4af88b516fcd14f5e7617a2eff53288af86db0a
7
+ data.tar.gz: 05ddb0a8850ceb30f4bd52b596e5e6ec237d1d03c70cdc6c12d1f01dd07d83ddfa1968f6db660fe7f00bbb60e41680095c6eeb993071c727f917be3d7a39e059
@@ -835,56 +835,6 @@ a.block-link:after {
835
835
  }
836
836
  }
837
837
 
838
- .form-input,.form-textarea,.form-select,.form-multiselect {
839
- -webkit-appearance: none;
840
- -moz-appearance: none;
841
- appearance: none;
842
- background-color: #fff;
843
- border-color: #6b7280;
844
- border-width: 1px;
845
- border-radius: 0px;
846
- padding-top: 0.5rem;
847
- padding-right: 0.75rem;
848
- padding-bottom: 0.5rem;
849
- padding-left: 0.75rem;
850
- font-size: 1rem;
851
- line-height: 1.5rem;
852
- --tw-shadow: 0 0 #0000;
853
- }
854
-
855
- .form-input:focus, .form-textarea:focus, .form-select:focus, .form-multiselect:focus {
856
- outline: 2px solid transparent;
857
- outline-offset: 2px;
858
- --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
859
- --tw-ring-offset-width: 0px;
860
- --tw-ring-offset-color: #fff;
861
- --tw-ring-color: #2563eb;
862
- --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
863
- --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
864
- box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
865
- border-color: #2563eb;
866
- }
867
-
868
- .form-select {
869
- background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
870
- background-position: right 0.5rem center;
871
- background-repeat: no-repeat;
872
- background-size: 1.5em 1.5em;
873
- padding-right: 2.5rem;
874
- -webkit-print-color-adjust: exact;
875
- print-color-adjust: exact;
876
- }
877
-
878
- .form-select:where([size]:not([size="1"])) {
879
- background-image: initial;
880
- background-position: initial;
881
- background-repeat: unset;
882
- background-size: initial;
883
- padding-right: 0.75rem;
884
- -webkit-print-color-adjust: unset;
885
- print-color-adjust: unset;
886
- }
887
-
888
838
  .aspect-h-7 {
889
839
  --tw-aspect-h: 7;
890
840
  }
@@ -10,12 +10,15 @@
10
10
  <% if @rows.any? %>
11
11
  <div class="table-row-group">
12
12
  <% @rows.each do |row| %>
13
- <div class="table-row relative bg-mid/5 hover:bg-mid/20">
13
+ <div class="table-row relative bg-mid/5 hover:bg-mid/20" data-post-id="<%= row.id %>">
14
14
  <% @columns.each do |column| %>
15
15
  <div class="table-cell py-5 px-3 h-20 text-sm align-middle whitespace-nowrap border-b border-mid/20">
16
16
  <%= view_context.capture(row, &column.cell) %>
17
17
  </div>
18
18
  <% end %>
19
+ <div class="table-cell py-5 px-3 h-20 text-sm align-middle whitespace-nowrap border-b border-mid/20">
20
+ <%= button_to "Delete", view_context.admin_post_path(row), method: :delete, class: "btn btn-danger", data: { confirm: "Are you sure?" } %>
21
+ </div>
19
22
  </div>
20
23
  <% end %>
21
24
  </div>
@@ -39,10 +39,11 @@ module Panda
39
39
  "editable-page-id": Current.page.id,
40
40
  "editable-block-content-id": block_content&.id
41
41
  }
42
- @options[:class] = "block bg-yellow-50 font-mono p-2 border-2 border-yellow-700"
42
+ @options[:class] = "block bg-yellow-50 font-mono text-xs p-2 border-2 border-yellow-700"
43
43
  @options[:style] = "white-space: pre-wrap;"
44
44
 
45
45
  @options[:id] = "editor-#{block_content&.id}"
46
+
46
47
  # TODO: Switch between the HTML and the preview?
47
48
  content_tag(:div, code_content, @options, true)
48
49
  else
@@ -1,6 +1,90 @@
1
+ <%
2
+ editor_data = if @editable
3
+ begin
4
+ content = if @content.is_a?(String)
5
+ if @content.start_with?("{")
6
+ JSON.parse(@content)
7
+ else
8
+ # If it's HTML content, convert it to EditorJS format
9
+ {
10
+ "time" => Time.current.to_i * 1000,
11
+ "blocks" => [
12
+ {
13
+ "type" => "paragraph",
14
+ "data" => {
15
+ "text" => @content.to_s
16
+ }
17
+ }
18
+ ],
19
+ "version" => "2.28.2"
20
+ }
21
+ end
22
+ else
23
+ @content
24
+ end
25
+
26
+ content = content.deep_transform_keys(&:to_s)
27
+ content["blocks"] = (content["blocks"] || []).map do |block|
28
+ case block["type"]
29
+ when "paragraph"
30
+ block["data"] = block["data"].merge(
31
+ "text" => block["data"]["text"].to_s.presence || ""
32
+ )
33
+ when "header"
34
+ block["data"] = block["data"].merge(
35
+ "text" => block["data"]["text"].to_s.presence || "",
36
+ "level" => block["data"]["level"].to_i
37
+ )
38
+ when "list"
39
+ block["data"] = block["data"].merge(
40
+ "items" => (block["data"]["items"] || []).map { |item| item.to_s.presence || "" }
41
+ )
42
+ end
43
+ block
44
+ end
45
+
46
+ content["version"] ||= "2.28.2"
47
+ content["time"] ||= Time.current.to_i * 1000
48
+
49
+ Base64.strict_encode64(content.to_json)
50
+ rescue StandardError => e
51
+ Rails.logger.error("Error encoding editor data: #{e.message}")
52
+ Rails.logger.error("Original content: #{@content.inspect}")
53
+ # Fall back to a simple paragraph with the original content
54
+ fallback_content = {
55
+ "time" => Time.current.to_i * 1000,
56
+ "blocks" => [
57
+ {
58
+ "type" => "paragraph",
59
+ "data" => {
60
+ "text" => @content.to_s
61
+ }
62
+ }
63
+ ],
64
+ "version" => "2.28.2"
65
+ }
66
+ Base64.strict_encode64(fallback_content.to_json)
67
+ end
68
+ end
69
+ %>
1
70
  <% if @editable %>
2
- <div class="panda-cms-content" data-editable-previous-data="<%= @content.to_json %>" id="editor-<%= @options[:id] %>" data-editable-kind="rich_text" data-editable-block-content-id="<%= @options[:id] %>" data-editable-page-id="<%= @options[:data][:page_id] %>" style="border: 1px dashed #ccc; outline: none; cursor: pointer; transition: background 500ms linear; background-color: inherit;">
71
+ <div class="panda-cms-content"
72
+ data-editable-previous-data="<%= editor_data %>"
73
+ data-editable-content="<%= editor_data %>"
74
+ data-editable-initialized="false"
75
+ data-editable-version="2.28.2"
76
+ data-editable-autosave="false"
77
+ data-editable-tools='{"paragraph":true,"header":true,"list":true,"quote":true,"table":true}'
78
+ id="editor-<%= @options[:id] %>"
79
+ data-editable-kind="rich_text"
80
+ data-editable-block-content-id="<%= @options[:id] %>"
81
+ data-editable-page-id="<%= @options[:data][:page_id] %>"
82
+ data-controller="editor-js"
83
+ data-editor-js-initialized-value="false"
84
+ data-editor-js-content-value="<%= editor_data %>">
3
85
  </div>
4
86
  <% else %>
5
- <div class="panda-cms-content"><%= @content %></div>
87
+ <div class="panda-cms-content">
88
+ <%= @content.presence || "<p></p>".html_safe %>
89
+ </div>
6
90
  <% end %>
@@ -35,49 +35,160 @@ module Panda
35
35
  block_content = Panda::CMS::BlockContent.create!(
36
36
  block: block,
37
37
  panda_cms_page_id: Current.page.id,
38
- content: ""
38
+ content: empty_editor_js_content
39
39
  )
40
40
  end
41
41
 
42
- @content = block_content.cached_content || block_content.content
43
- raise ComponentError, "No content found for block: #{block.id}" if @content.nil?
44
-
42
+ raw_content = block_content.cached_content || block_content.content
43
+ @content = raw_content.presence || empty_editor_js_content
45
44
  @options[:id] = block_content.id
46
45
 
46
+ # Debug log the content
47
+ Rails.logger.debug("RichTextComponent content before processing: #{@content.inspect}")
48
+
47
49
  if @editable
48
50
  @options[:data] = {
49
51
  page_id: Current.page.id,
50
52
  mode: "rich_text"
51
53
  }
52
54
 
53
- # Convert HTML content to EditorJS format if needed
54
- begin
55
- editor_content = Panda::CMS::HtmlToEditorJsConverter.convert(@content)
56
- @content = editor_content
57
- rescue Panda::CMS::HtmlToEditorJsConverter::ConversionError => e
58
- raise ComponentError, "Failed to convert content: #{e.message}"
59
- end
60
- elsif @content.is_a?(Hash)
61
- begin
62
- renderer = Panda::CMS::EditorJs::Renderer.new(@content)
63
- @content = renderer.render
64
- rescue => e
65
- raise ComponentError, "Failed to render content: #{e.message}"
55
+ # For editable mode, always ensure we have a valid EditorJS structure
56
+ @content = if @content.blank? || @content == "{}"
57
+ empty_editor_js_content
58
+ else
59
+ begin
60
+ if @content.is_a?(String)
61
+ # Try to parse as JSON first
62
+ begin
63
+ parsed = JSON.parse(@content)
64
+ if valid_editor_js_content?(parsed)
65
+ # Ensure the content is properly structured
66
+ {
67
+ "time" => parsed["time"] || Time.current.to_i * 1000,
68
+ "blocks" => parsed["blocks"].map { |block|
69
+ {
70
+ "type" => block["type"],
71
+ "data" => block["data"].merge(
72
+ "text" => block["data"]["text"].to_s.presence || ""
73
+ ),
74
+ "tunes" => block["tunes"]
75
+ }.compact
76
+ },
77
+ "version" => parsed["version"] || "2.28.2"
78
+ }
79
+ else
80
+ # If not valid EditorJS, try to convert from HTML
81
+ begin
82
+ editor_content = Panda::CMS::HtmlToEditorJsConverter.convert(@content)
83
+ if valid_editor_js_content?(editor_content)
84
+ editor_content
85
+ else
86
+ empty_editor_js_content
87
+ end
88
+ rescue Panda::CMS::HtmlToEditorJsConverter::ConversionError => e
89
+ Rails.logger.error("HTML conversion error: #{e.message}")
90
+ empty_editor_js_content
91
+ end
92
+ end
93
+ rescue JSON::ParserError => e
94
+ Rails.logger.error("JSON parse error: #{e.message}")
95
+ # Try to convert from HTML
96
+ begin
97
+ editor_content = Panda::CMS::HtmlToEditorJsConverter.convert(@content)
98
+ if valid_editor_js_content?(editor_content)
99
+ editor_content
100
+ else
101
+ empty_editor_js_content
102
+ end
103
+ rescue Panda::CMS::HtmlToEditorJsConverter::ConversionError => e
104
+ Rails.logger.error("HTML conversion error: #{e.message}")
105
+ empty_editor_js_content
106
+ end
107
+ end
108
+ else
109
+ # If it's not a string, assume it's already in the correct format
110
+ valid_editor_js_content?(@content) ? @content : empty_editor_js_content
111
+ end
112
+ rescue => e
113
+ Rails.logger.error("Content processing error: #{e.message}\nContent: #{@content.inspect}")
114
+ empty_editor_js_content
115
+ end
66
116
  end
67
117
  else
68
- @content = @content.html_safe
118
+ # For non-editable mode, handle content display
119
+ @content = if @content.blank? || @content == "{}"
120
+ "<p></p>".html_safe
121
+ else
122
+ begin
123
+ # Try to parse as JSON if it looks like EditorJS format
124
+ if @content.is_a?(String) && @content.strip.match?(/^\{.*"blocks":\s*\[.*\].*\}$/m)
125
+ parsed_content = JSON.parse(@content)
126
+ if valid_editor_js_content?(parsed_content)
127
+ # Check if it's just an empty paragraph
128
+ if parsed_content["blocks"].length == 1 &&
129
+ parsed_content["blocks"][0]["type"] == "paragraph" &&
130
+ parsed_content["blocks"][0]["data"]["text"].blank?
131
+ "<p></p>".html_safe
132
+ else
133
+ renderer = Panda::CMS::EditorJs::Renderer.new(parsed_content)
134
+ rendered = renderer.render
135
+ rendered.presence&.html_safe || "<p></p>".html_safe
136
+ end
137
+ else
138
+ process_html(@content)
139
+ end
140
+ else
141
+ process_html(@content)
142
+ end
143
+ rescue JSON::ParserError
144
+ process_html(@content)
145
+ rescue => e
146
+ Rails.logger.error("RichTextComponent render error: #{e.message}\nContent: #{@content.inspect}")
147
+ "<p></p>".html_safe
148
+ end
149
+ end
69
150
  end
70
151
  rescue ActiveRecord::RecordNotFound => e
71
152
  raise ComponentError, "Database record not found: #{e.message}"
72
153
  rescue ActiveRecord::RecordInvalid => e
73
154
  raise ComponentError, "Invalid record: #{e.message}"
74
155
  rescue => e
75
- raise ComponentError, "Component error: #{e.message}"
156
+ Rails.logger.error("RichTextComponent error: #{e.message}\nContent: #{@content.inspect}")
157
+ @content = @editable ? empty_editor_js_content : "<p></p>".html_safe
158
+ nil
159
+ end
160
+
161
+ private
162
+
163
+ def empty_editor_js_content
164
+ {
165
+ time: Time.current.to_i * 1000,
166
+ blocks: [{type: "paragraph", data: {text: ""}}],
167
+ version: "2.28.2"
168
+ }
169
+ end
170
+
171
+ def valid_editor_js_content?(content)
172
+ content.is_a?(Hash) && content["blocks"].is_a?(Array) && content["version"].present?
173
+ rescue
174
+ false
175
+ end
176
+
177
+ def process_html(content)
178
+ return "<p></p>".html_safe if content.blank?
179
+
180
+ # If it's already HTML, just return it
181
+ if content.match?(/<[^>]+>/)
182
+ content.html_safe
183
+ else
184
+ # Wrap plain text in paragraph tags
185
+ "<p>#{content}</p>".html_safe
186
+ end
76
187
  end
77
188
 
78
189
  # Only render the component if there is some content set, or if the component is editable
79
190
  def render?
80
- @content.present? || @editable
191
+ true # Always render, we'll show empty content if needed
81
192
  end
82
193
  end
83
194
  end
@@ -15,15 +15,26 @@ module Panda
15
15
  Rails.logger.debug "Content params: #{params.inspect}"
16
16
  Rails.logger.debug "Raw content: #{request.raw_post}"
17
17
 
18
- if @block_content.update!(content: params.dig(:content))
19
- @block_content.page.touch
20
- render json: @block_content, status: :ok
18
+ # Ensure content isn't HTML escaped before saving
19
+ if params[:content].present?
20
+ # Convert ActionController::Parameters to a string if needed
21
+ content_str = params[:content].is_a?(ActionController::Parameters) ? params[:content].to_json : params[:content].to_s
22
+ content = CGI.unescapeHTML(content_str)
21
23
  else
22
- render json: @block_content.errors, status: :unprocessable_entity
24
+ content = nil
25
+ end
26
+
27
+ begin
28
+ if content && @block_content.update!(content: content)
29
+ @block_content.page.touch
30
+ render json: @block_content, status: :ok
31
+ else
32
+ render json: @block_content.errors, status: :unprocessable_entity
33
+ end
34
+ rescue => e
35
+ Rails.logger.error "Error updating block content: #{e.message}"
36
+ render json: {error: e.message}, status: :unprocessable_entity
23
37
  end
24
- rescue => e
25
- Rails.logger.error "Error updating block content: #{e.message}"
26
- render json: {error: e.message}, status: :unprocessable_entity
27
38
  end
28
39
 
29
40
  private
@@ -1,20 +1,30 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module CMS
3
- class Admin::FilesController < ApplicationController
4
- before_action :set_initial_breadcrumb, only: %i[index show]
5
- before_action :authenticate_admin_user!
6
-
7
- def index
8
- redirect_to admin_dashboard_path
9
- end
5
+ module Admin
6
+ class FilesController < ApplicationController
7
+ before_action :authenticate_admin_user!
10
8
 
11
- def show
12
- end
9
+ def create
10
+ file = params[:image]
11
+ return render json: {success: 0} unless file
13
12
 
14
- private
13
+ blob = ActiveStorage::Blob.create_and_upload!(
14
+ io: file,
15
+ filename: file.original_filename,
16
+ content_type: file.content_type
17
+ )
15
18
 
16
- def set_initial_breadcrumb
17
- add_breadcrumb "Media", admin_files_path
19
+ render json: {
20
+ success: true,
21
+ file: {
22
+ url: Rails.application.routes.url_helpers.rails_blob_url(blob, only_path: true),
23
+ name: blob.filename.to_s,
24
+ size: blob.byte_size
25
+ }
26
+ }
27
+ end
18
28
  end
19
29
  end
20
30
  end
@@ -14,7 +14,7 @@ module Panda
14
14
  # @type GET
15
15
  # @return ActiveRecord::Collection A list of all posts
16
16
  def index
17
- posts = Panda::CMS::Post.with_user.ordered
17
+ posts = Panda::CMS::Post.with_author.ordered
18
18
  render :index, locals: {posts: posts}
19
19
  end
20
20
 
@@ -48,16 +48,12 @@ module Panda
48
48
  # POST /admin/posts
49
49
  def create
50
50
  @post = Panda::CMS::Post.new(post_params)
51
-
52
- begin
53
- @post.content = JSON.parse(post_params[:content])
54
- rescue
55
- @post.content = post_params[:content]
56
- end
51
+ @post.user_id = current_user.id
52
+ @post.content = parse_content(post_params[:content]) # Parse the content before saving
57
53
 
58
54
  if @post.save
59
55
  Rails.logger.debug "Post saved successfully"
60
- redirect_to edit_admin_post_path(@post.admin_param), notice: "Post was successfully created."
56
+ redirect_to edit_admin_post_path(@post.admin_param), success: "The post was successfully created!"
61
57
  else
62
58
  Rails.logger.debug "Post save failed: #{@post.errors.full_messages.inspect}"
63
59
  flash.now[:error] = @post.errors.full_messages.join(", ")
@@ -69,11 +65,14 @@ module Panda
69
65
  # @type PATCH/PUT
70
66
  # @return
71
67
  def update
72
- Rails.logger.debug "Updating post with params: #{post_params.inspect}"
73
68
  Rails.logger.debug "Current content: #{post.content.inspect}"
74
69
  Rails.logger.debug "New content from params: #{post_params[:content].inspect}"
75
70
 
76
- if post.update(post_params)
71
+ # Parse the content before updating
72
+ update_params = post_params
73
+ update_params[:content] = parse_content(post_params[:content])
74
+ update_params[:user_id] = current_user.id
75
+ if post.update(update_params)
77
76
  Rails.logger.debug "Post updated successfully"
78
77
  add_breadcrumb post.title, edit_admin_post_path(post.admin_param)
79
78
  redirect_to edit_admin_post_path(post.admin_param),
@@ -136,10 +135,33 @@ module Panda
136
135
  :slug,
137
136
  :status,
138
137
  :published_at,
139
- :user_id,
138
+ :author_id,
140
139
  :content
141
140
  )
142
141
  end
142
+
143
+ def parse_content(content)
144
+ return {} if content.blank?
145
+
146
+ begin
147
+ # If content is already a hash, return it
148
+ return content if content.is_a?(Hash)
149
+
150
+ # If it's a string, try to parse it as JSON
151
+ parsed = JSON.parse(content)
152
+
153
+ # Ensure we have a hash with expected structure
154
+ if parsed.is_a?(Hash) && parsed["blocks"].is_a?(Array)
155
+ parsed
156
+ else
157
+ # If structure is invalid, return empty blocks structure
158
+ {"blocks" => []}
159
+ end
160
+ rescue JSON::ParserError => e
161
+ Rails.logger.error "Failed to parse post content: #{e.message}"
162
+ {"blocks" => []}
163
+ end
164
+ end
143
165
  end
144
166
  end
145
167
  end
@@ -4,6 +4,7 @@ module Panda
4
4
  include ActionView::Helpers::TagHelper
5
5
 
6
6
  before_action :check_login_required, only: [:root, :show]
7
+ before_action :handle_redirects, only: [:root, :show]
7
8
  after_action :record_visit, only: [:root, :show], unless: :ignore_visit?
8
9
 
9
10
  def root
@@ -40,6 +41,24 @@ module Panda
40
41
 
41
42
  private
42
43
 
44
+ def handle_redirects
45
+ current_path = "/" + params[:path].to_s
46
+ redirect = Panda::CMS::Redirect.find_by(origin_path: current_path)
47
+
48
+ if redirect
49
+ redirect.increment!(:visits)
50
+
51
+ # Check if the destination is also a redirect
52
+ next_redirect = Panda::CMS::Redirect.find_by(origin_path: redirect.destination_path)
53
+ if next_redirect
54
+ next_redirect.increment!(:visits)
55
+ redirect_to next_redirect.destination_path, status: redirect.status_code and return
56
+ end
57
+
58
+ redirect_to redirect.destination_path, status: redirect.status_code and return
59
+ end
60
+ end
61
+
43
62
  def check_login_required
44
63
  if Panda::CMS.config.require_login_to_view && !user_signed_in?
45
64
  redirect_to panda_cms_maintenance_path and return
@@ -67,6 +86,16 @@ module Panda
67
86
  visited_at: Time.zone.now
68
87
  )
69
88
  end
89
+
90
+ def create_redirect_if_path_changed
91
+ if path_changed? && path_was.present?
92
+ Panda::CMS::Redirect.create!(
93
+ origin_path: path_was,
94
+ destination_path: path,
95
+ status_code: 301
96
+ )
97
+ end
98
+ end
70
99
  end
71
100
  end
72
101
  end
@@ -1,12 +1,34 @@
1
1
  module Panda
2
2
  module CMS
3
3
  class PostsController < ApplicationController
4
+ # TODO: Change from layout rendering to standard template rendering
5
+ # inside a /panda/cms/posts/... structure in the application
6
+ def index
7
+ @posts = Panda::CMS::Post.includes(:author).order(published_at: :desc)
8
+ render inline: "", layout: Panda::CMS.config.posts[:layouts][:index]
9
+ end
10
+
4
11
  def show
5
- @posts_index_page = Panda::CMS::Page.find_by(path: "/#{Panda::CMS.config.posts[:prefix]}")
6
- @post = Panda::CMS::Post.find_by!(slug: "/#{params[:slug]}")
7
- @title = @post.title
12
+ @post = if params[:year] && params[:month]
13
+ # For date-based URLs
14
+ slug = "/#{params[:year]}/#{params[:month]}/#{params[:slug]}"
15
+ Panda::CMS::Post.find_by!(slug: slug)
16
+ else
17
+ # For non-date URLs
18
+ Panda::CMS::Post.find_by!(slug: "/#{params[:slug]}")
19
+ end
20
+ render inline: "", layout: Panda::CMS.config.posts[:layouts][:show]
21
+ end
22
+
23
+ def by_month
24
+ @month = Date.new(params[:year].to_i, params[:month].to_i, 1)
25
+ @posts = Panda::CMS::Post
26
+ .where(status: :active)
27
+ .where("DATE_TRUNC('month', published_at) = ?", @month)
28
+ .includes(:author)
29
+ .ordered
8
30
 
9
- render inline: "", status: :ok, layout: "layouts/post"
31
+ render inline: "", layout: Panda::CMS.config.posts[:layouts][:by_month]
10
32
  end
11
33
  end
12
34
  end