panda-cms 0.7.0 → 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) 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 +1 -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 +3 -6
  32. data/lib/panda-cms/version.rb +1 -1
  33. data/lib/panda-cms.rb +5 -11
  34. metadata +290 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c002856b3c2b19f78b45a5fd2bb58287bd8b49cfc305f4e400c52e41c38fa9b
4
- data.tar.gz: df33fb1880dab8f2361d36859a2c0a556710d2c5879f3a0ae052b4ea7a4e0d78
3
+ metadata.gz: e27fb8ea71f2ea75ce8b82dc1a572e9fc6c55ee7d0e1df48b3af93228881bc0e
4
+ data.tar.gz: a58147bb5374e6bfdc70aed581c9b98048b0e3937a4037c2daae7e5b6c70ae57
5
5
  SHA512:
6
- metadata.gz: db39a72f704c8aebd0b1049b6efba4c41ab5fb09dc8320de6693848829ef2b4ce3e4a90d081a38ce5a44ec6a7e21cae4edf1ce122a678d2d1e4aa85be28de757
7
- data.tar.gz: 1b3260d6ca7bffbfa794894f90f5db94fb71a47510bf3eeef599264883baee32d52e1527e06492d372c30f790920d6688d02644d2d186cfe59c9bbd380fad431
6
+ metadata.gz: e6494d67893174959b47efe2276c1fe1ae27606cc268e62d1bf77566bcbf95c37cc935f2a62c0bdc0db24f62b42d2ca44cc36c8a9a2a328bee634d5e079374c1
7
+ data.tar.gz: 2d263e8ca147f3c0bc9061f5f9c7149d4705a7b16fbc1d02a7072a71884292574677a588b9dc9dbbae30d90da6e2495f6b43f319fe674b98da2076fe97a64cc7
@@ -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,7 +10,7 @@
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) %>
@@ -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