panda_cms 0.5.10 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +3 -1
- data/Rakefile +0 -1
- data/app/assets/builds/panda_cms.css +2432 -1
- data/app/assets/config/panda_cms_manifest.js +3 -0
- data/app/assets/stylesheets/panda_cms/application.tailwind.css +3 -27
- data/app/builders/panda_cms/form_builder.rb +1 -1
- data/app/components/panda_cms/admin/button_component.rb +6 -3
- data/app/components/panda_cms/admin/flash_message_component.rb +1 -1
- data/app/components/panda_cms/admin/tag_component.rb +1 -1
- data/app/components/panda_cms/code_component.rb +62 -0
- data/app/components/panda_cms/menu_component.html.erb +3 -0
- data/app/components/panda_cms/menu_component.rb +8 -1
- data/app/components/panda_cms/page_menu_component.html.erb +12 -8
- data/app/components/panda_cms/page_menu_component.rb +24 -13
- data/app/components/panda_cms/rich_text_component.html.erb +6 -38
- data/app/components/panda_cms/rich_text_component.rb +32 -7
- data/app/components/panda_cms/text_component.rb +25 -22
- data/app/controllers/panda_cms/admin/block_contents_controller.rb +1 -1
- data/app/controllers/panda_cms/admin/dashboard_controller.rb +14 -6
- data/app/controllers/panda_cms/admin/menus_controller.rb +1 -54
- data/app/controllers/panda_cms/admin/pages_controller.rb +2 -1
- data/app/controllers/panda_cms/admin/sessions_controller.rb +13 -6
- data/app/controllers/panda_cms/application_controller.rb +1 -1
- data/app/controllers/panda_cms/pages_controller.rb +1 -1
- data/app/controllers/panda_cms/posts_controller.rb +1 -1
- data/app/helpers/panda_cms/application_helper.rb +2 -2
- data/app/javascript/panda_cms/@editorjs--editorjs.js +2577 -0
- data/app/javascript/panda_cms/@hotwired--stimulus.js +4 -0
- data/app/javascript/panda_cms/@hotwired--turbo.js +160 -0
- data/app/javascript/panda_cms/@rails--actioncable--src.js +4 -0
- data/app/javascript/panda_cms/application_panda_cms.js +4 -0
- data/app/javascript/panda_cms/controllers/dashboard_controller.js +7 -0
- data/app/javascript/panda_cms/controllers/editor_controller.js +247 -0
- data/app/javascript/panda_cms/controllers/index.js +45 -0
- data/app/javascript/panda_cms/controllers/slug_controller.js +48 -0
- data/app/javascript/panda_cms/editor/plain_text_editor.js +102 -0
- data/app/javascript/panda_cms/editor/resource_loader.js +69 -0
- data/app/javascript/panda_cms/editor/rich_text_editor.js +89 -0
- data/app/javascript/panda_cms/tailwindcss-stimulus-components.js +4 -0
- data/app/lib/panda_cms/demo_site_generator.rb +1 -3
- data/app/lib/panda_cms/editor_js/blocks/alert.rb +32 -0
- data/app/lib/panda_cms/editor_js/blocks/base.rb +28 -0
- data/app/lib/panda_cms/editor_js/blocks/header.rb +13 -0
- data/app/lib/panda_cms/editor_js/blocks/image.rb +34 -0
- data/app/lib/panda_cms/editor_js/blocks/list.rb +30 -0
- data/app/lib/panda_cms/editor_js/blocks/paragraph.rb +13 -0
- data/app/lib/panda_cms/editor_js/blocks/quote.rb +27 -0
- data/app/lib/panda_cms/editor_js/blocks/table.rb +48 -0
- data/app/lib/panda_cms/editor_js/renderer.rb +120 -0
- data/app/lib/panda_cms/slug.rb +1 -1
- data/app/models/panda_cms/block.rb +2 -2
- data/app/models/panda_cms/block_content.rb +12 -2
- data/app/models/panda_cms/page.rb +9 -3
- data/app/models/panda_cms/post.rb +1 -1
- data/app/models/panda_cms/template.rb +4 -2
- data/app/models/panda_cms/user.rb +9 -1
- data/app/views/panda_cms/admin/dashboard/show.html.erb +11 -9
- data/app/views/panda_cms/admin/forms/new.html.erb +6 -7
- data/app/views/panda_cms/admin/menus/index.html.erb +0 -2
- data/app/views/panda_cms/admin/pages/edit.html.erb +22 -19
- data/app/views/panda_cms/admin/pages/new.html.erb +6 -7
- data/app/views/panda_cms/admin/posts/_form.html.erb +4 -4
- data/app/views/panda_cms/admin/sessions/new.html.erb +1 -2
- data/app/views/panda_cms/admin/shared/_sidebar.html.erb +12 -16
- data/app/views/panda_cms/shared/_header.html.erb +13 -14
- data/app/views/panda_cms/shared/_importmap.html.erb +32 -0
- data/config/importmap.rb +13 -10
- data/config/initializers/panda_cms.rb +57 -55
- data/config/routes.rb +9 -9
- data/config/tailwind.config.js +1 -0
- data/db/migrate/20240205223709_create_panda_cms_pages.rb +6 -4
- data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +9 -0
- data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +5 -0
- data/db/seeds.rb +1 -0
- data/lib/generators/panda_cms/install_generator.rb +3 -0
- data/lib/panda_cms/engine.rb +42 -29
- data/lib/panda_cms/version.rb +1 -1
- data/lib/panda_cms.rb +58 -10
- data/lib/tasks/panda_cms.rake +41 -57
- data/public/panda-cms-assets/rich_text_editor.css +568 -0
- metadata +228 -304
- data/app/javascript/base.js +0 -37
- data/app/javascript/controllers/menu_controller.js +0 -19
- data/app/javascript/controllers/text_controller.js +0 -78
- data/app/javascript/controllers/text_field_update_controller.js +0 -51
- data/app/javascript/vendor/stimulus-components-rails-nested-form.js +0 -2
- data/app/javascript/vendor/tailwindcss-stimulus-components.js +0 -2
- data/app/views/panda_cms/admin/menus/_form.html.erb +0 -21
- data/app/views/panda_cms/admin/menus/_menu_item_fields.html.erb +0 -7
- data/app/views/panda_cms/admin/menus/edit.html.erb +0 -58
- data/app/views/panda_cms/admin/menus/new.html.erb +0 -5
- data/db/migrate/20240804110225_add_status_to_panda_cms_pages.rb +0 -7
- data/public/panda-cms-assets/javascripts/base.js +0 -37
- data/public/panda-cms-assets/javascripts/controllers/menu_controller.js +0 -19
- data/public/panda-cms-assets/javascripts/controllers/text_field_update_controller.js +0 -23
- data/public/panda-cms-assets/javascripts/embed/editable.js +0 -358
- data/public/panda-cms-assets/javascripts/embed/rich_text.css +0 -1294
- data/public/panda-cms-assets/javascripts/vendor/stimulus-components-rails-nested-form.js +0 -2
- data/public/panda-cms-assets/javascripts/vendor/stimulus-loading.js +0 -113
- data/public/panda-cms-assets/javascripts/vendor/tailwindcss-stimulus-components.js +0 -2
@@ -0,0 +1,32 @@
|
|
1
|
+
module PandaCms
|
2
|
+
module EditorJs
|
3
|
+
module Blocks
|
4
|
+
class Alert < Base
|
5
|
+
def render
|
6
|
+
message = sanitize(data["message"])
|
7
|
+
type = data["type"] || "primary"
|
8
|
+
|
9
|
+
html_safe(
|
10
|
+
"<div class=\"#{alert_classes(type)} p-4 mb-4 rounded-lg\">" \
|
11
|
+
"#{message}" \
|
12
|
+
"</div>"
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def alert_classes(type)
|
19
|
+
case type
|
20
|
+
when "primary" then "bg-blue-100 text-blue-800"
|
21
|
+
when "secondary" then "bg-gray-100 text-gray-800"
|
22
|
+
when "success" then "bg-green-100 text-green-800"
|
23
|
+
when "danger" then "bg-red-100 text-red-800"
|
24
|
+
when "warning" then "bg-yellow-100 text-yellow-800"
|
25
|
+
when "info" then "bg-indigo-100 text-indigo-800"
|
26
|
+
else "bg-blue-100 text-blue-800"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module PandaCms
|
2
|
+
module EditorJs
|
3
|
+
module Blocks
|
4
|
+
class Base
|
5
|
+
attr_reader :data, :options
|
6
|
+
|
7
|
+
def initialize(data, options = {})
|
8
|
+
@data = data
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def render
|
13
|
+
""
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def sanitize(text)
|
19
|
+
Rails::Html::SafeListSanitizer.new.sanitize(text, tags: %w[b i u a code])
|
20
|
+
end
|
21
|
+
|
22
|
+
def html_safe(text)
|
23
|
+
text.to_s.html_safe
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module PandaCms
|
2
|
+
module EditorJs
|
3
|
+
module Blocks
|
4
|
+
class Image < Base
|
5
|
+
def render
|
6
|
+
url = data["url"]
|
7
|
+
caption = sanitize(data["caption"])
|
8
|
+
with_border = data["withBorder"]
|
9
|
+
with_background = data["withBackground"]
|
10
|
+
stretched = data["stretched"]
|
11
|
+
|
12
|
+
css_classes = ["prose"]
|
13
|
+
css_classes << "border" if with_border
|
14
|
+
css_classes << "bg-gray-100" if with_background
|
15
|
+
css_classes << "w-full" if stretched
|
16
|
+
|
17
|
+
html_safe(<<~HTML)
|
18
|
+
<figure class="#{css_classes.join(" ")}">
|
19
|
+
<img src="#{url}" alt="#{caption}" />
|
20
|
+
#{caption_element(caption)}
|
21
|
+
</figure>
|
22
|
+
HTML
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def caption_element(caption)
|
28
|
+
return "" if caption.blank?
|
29
|
+
"<figcaption>#{caption}</figcaption>"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module PandaCms
|
2
|
+
module EditorJs
|
3
|
+
module Blocks
|
4
|
+
class List < Base
|
5
|
+
def render
|
6
|
+
list_type = (data["style"] == "ordered") ? "ol" : "ul"
|
7
|
+
html_safe(
|
8
|
+
"<#{list_type}>" \
|
9
|
+
"#{render_items(data["items"])}" \
|
10
|
+
"</#{list_type}>"
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def render_items(items)
|
17
|
+
items.map do |item|
|
18
|
+
content = item.is_a?(Hash) ? item["content"] : item
|
19
|
+
nested = (item.is_a?(Hash) && item["items"].present?) ? render_nested(item["items"]) : ""
|
20
|
+
"<li>#{sanitize(content)}#{nested}</li>"
|
21
|
+
end.join
|
22
|
+
end
|
23
|
+
|
24
|
+
def render_nested(items)
|
25
|
+
self.class.new({"items" => items, "style" => data["style"]}).render
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module PandaCms
|
2
|
+
module EditorJs
|
3
|
+
module Blocks
|
4
|
+
class Quote < Base
|
5
|
+
def render
|
6
|
+
text = sanitize(data["text"])
|
7
|
+
caption = sanitize(data["caption"])
|
8
|
+
alignment = data["alignment"] || "left"
|
9
|
+
|
10
|
+
html_safe(
|
11
|
+
"<figure class=\"text-#{alignment}\">" \
|
12
|
+
"<blockquote><p>#{text}</p></blockquote>" \
|
13
|
+
"#{caption_element(caption)}" \
|
14
|
+
"</figure>"
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def caption_element(caption)
|
21
|
+
return "" if caption.blank?
|
22
|
+
"<figcaption>#{caption}</figcaption>"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module PandaCms
|
2
|
+
module EditorJs
|
3
|
+
module Blocks
|
4
|
+
class Table < Base
|
5
|
+
def render
|
6
|
+
content = data["content"]
|
7
|
+
with_headings = data["withHeadings"]
|
8
|
+
|
9
|
+
html_safe(<<~HTML)
|
10
|
+
<div class="overflow-x-auto">
|
11
|
+
<table class="min-w-full">
|
12
|
+
#{render_rows(content, with_headings)}
|
13
|
+
</table>
|
14
|
+
</div>
|
15
|
+
HTML
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def render_rows(content, with_headings)
|
21
|
+
rows = []
|
22
|
+
index = 0
|
23
|
+
|
24
|
+
while index < content.length
|
25
|
+
rows << if index == 0 && with_headings
|
26
|
+
render_header_row(content[index])
|
27
|
+
else
|
28
|
+
render_data_row(content[index])
|
29
|
+
end
|
30
|
+
index += 1
|
31
|
+
end
|
32
|
+
|
33
|
+
rows.join("\n")
|
34
|
+
end
|
35
|
+
|
36
|
+
def render_header_row(row)
|
37
|
+
cells = row.map { |cell| "<th>#{sanitize(cell)}</th>" }
|
38
|
+
"<tr>#{cells.join}</tr>"
|
39
|
+
end
|
40
|
+
|
41
|
+
def render_data_row(row)
|
42
|
+
cells = row.map { |cell| "<td>#{sanitize(cell)}</td>" }
|
43
|
+
"<tr>#{cells.join}</tr>"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module PandaCms
|
2
|
+
module EditorJs
|
3
|
+
class Renderer
|
4
|
+
attr_reader :content, :options, :custom_renderers, :cache_store
|
5
|
+
|
6
|
+
def initialize(content, options = {})
|
7
|
+
@content = content
|
8
|
+
@options = options
|
9
|
+
@custom_renderers = options.delete(:custom_renderers) || {}
|
10
|
+
@cache_store = options.delete(:cache_store) || Rails.cache
|
11
|
+
@validate_html = options.delete(:validate_html) || false
|
12
|
+
end
|
13
|
+
|
14
|
+
def render
|
15
|
+
return "" if content.nil? || !content.is_a?(Hash) || !content["blocks"]
|
16
|
+
|
17
|
+
blocks = remove_empty_paragraphs(content["blocks"])
|
18
|
+
rendered = blocks.map { |block| render_block_with_cache(block) }.join("\n")
|
19
|
+
|
20
|
+
@validate_html ? validate_html(rendered) : rendered
|
21
|
+
end
|
22
|
+
|
23
|
+
def section(blocks)
|
24
|
+
"<section class=\"content-section\">#{render_blocks(blocks)}</section>"
|
25
|
+
end
|
26
|
+
|
27
|
+
def article(blocks, title: nil)
|
28
|
+
content = []
|
29
|
+
content << "<h1>#{title}</h1>" if title
|
30
|
+
content << render_blocks(blocks)
|
31
|
+
"<article>#{content.join("\n")}</article>"
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def render_blocks(blocks)
|
37
|
+
blocks.map { |block| render_block_with_cache(block) }.join("\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
def validate_html(html)
|
41
|
+
# First check if we have matching numbers of opening and closing tags
|
42
|
+
opening_tags = html.scan(/<([a-z]+)[^>]*>/i)
|
43
|
+
closing_tags = html.scan(/<\/([a-z]+)>/i)
|
44
|
+
|
45
|
+
# Early return if tag counts don't match
|
46
|
+
return "" unless opening_tags.length == closing_tags.length
|
47
|
+
|
48
|
+
# Check tag order and nesting
|
49
|
+
stack = []
|
50
|
+
tag_pattern = /<\/?([a-z]+)[^>]*>/i
|
51
|
+
position = 0
|
52
|
+
|
53
|
+
while (match = html[position..].match(tag_pattern))
|
54
|
+
tag_name = match[1].downcase
|
55
|
+
is_closing = match[0].start_with?("</")
|
56
|
+
|
57
|
+
if is_closing
|
58
|
+
return "" if stack.pop != tag_name
|
59
|
+
else
|
60
|
+
stack.push(tag_name)
|
61
|
+
end
|
62
|
+
|
63
|
+
position += match.begin(0) + match[0].length
|
64
|
+
end
|
65
|
+
|
66
|
+
stack.empty? ? html : ""
|
67
|
+
end
|
68
|
+
|
69
|
+
def render_block_with_cache(block)
|
70
|
+
return "" if @validate_html && has_invalid_html?(block["data"])
|
71
|
+
|
72
|
+
cache_key = "editor_js_block/#{block["type"]}/#{Digest::MD5.hexdigest(block["data"].to_json)}"
|
73
|
+
cache_store.fetch(cache_key) do
|
74
|
+
renderer_for(block).render
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def remove_empty_paragraphs(blocks)
|
79
|
+
blocks.reject do |block|
|
80
|
+
block["type"] == "paragraph" &&
|
81
|
+
block["data"]["text"].blank? &&
|
82
|
+
(blocks.last == block || next_block_is_empty?(blocks, block))
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def next_block_is_empty?(blocks, current_block)
|
87
|
+
current_index = blocks.index(current_block)
|
88
|
+
next_block = blocks[current_index + 1]
|
89
|
+
next_block && next_block["type"] == "paragraph" && next_block["data"]["text"].blank?
|
90
|
+
end
|
91
|
+
|
92
|
+
def renderer_for(block)
|
93
|
+
if custom_renderers[block["type"]]
|
94
|
+
custom_renderers[block["type"]].new(block["data"], options)
|
95
|
+
else
|
96
|
+
default_renderer_for(block)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def default_renderer_for(block)
|
101
|
+
renderer_class = "PandaCms::EditorJs::Blocks::#{block["type"].classify}".constantize
|
102
|
+
renderer_class.new(block["data"], options)
|
103
|
+
rescue NameError
|
104
|
+
PandaCms::EditorJs::Blocks::Base.new(block["data"], options)
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def has_invalid_html?(data)
|
110
|
+
data.values.any? do |value|
|
111
|
+
next unless value.is_a?(String)
|
112
|
+
opening_tags = value.scan(/<([a-z]+)[^>]*>/i)
|
113
|
+
closing_tags = value.scan(/<\/([a-z]+)>/i)
|
114
|
+
opening_tags.length != closing_tags.length ||
|
115
|
+
opening_tags.map(&:first) != closing_tags.map(&:first)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
data/app/lib/panda_cms/slug.rb
CHANGED
@@ -5,7 +5,7 @@ module PandaCms
|
|
5
5
|
#
|
6
6
|
# @param string [String] The provided string to turn into a slug
|
7
7
|
# @return string Generated slug
|
8
|
-
# @see
|
8
|
+
# @see slug_controller.js should also implement this logic
|
9
9
|
def self.generate(string)
|
10
10
|
# Trim whitespace and downcase the string
|
11
11
|
string = string.to_s.strip.downcase
|
@@ -16,12 +16,12 @@ module PandaCms
|
|
16
16
|
plain_text: "plain_text",
|
17
17
|
rich_text: "rich_text",
|
18
18
|
iframe: "iframe",
|
19
|
-
list: "list"
|
19
|
+
list: "list",
|
20
|
+
code: "code"
|
20
21
|
# image: "image",
|
21
22
|
# video: "video",
|
22
23
|
# audio: "audio",
|
23
24
|
# file: "file",
|
24
|
-
# code: "code",
|
25
25
|
# iframe: "iframe",
|
26
26
|
# quote: "quote",
|
27
27
|
# list: "list"
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require "redcarpet"
|
2
|
-
|
3
1
|
module PandaCms
|
4
2
|
class BlockContent < ApplicationRecord
|
5
3
|
self.table_name = "panda_cms_block_contents"
|
@@ -12,5 +10,17 @@ module PandaCms
|
|
12
10
|
belongs_to :block, foreign_key: :panda_cms_block_id, class_name: "PandaCms::Block", inverse_of: :block_contents, optional: false
|
13
11
|
|
14
12
|
validates :block, presence: true, uniqueness: {scope: :page}
|
13
|
+
|
14
|
+
before_save :generate_cache
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def generate_cache
|
19
|
+
self.cached_content = if content.is_a?(Hash) && content.dig("source") == "editorJS"
|
20
|
+
EditorJs::Renderer.new(content).render
|
21
|
+
else
|
22
|
+
content
|
23
|
+
end
|
24
|
+
end
|
15
25
|
end
|
16
26
|
end
|
@@ -18,15 +18,21 @@ module PandaCms
|
|
18
18
|
has_many :menu_items, foreign_key: :panda_cms_page_id, class_name: "PandaCms::MenuItem", inverse_of: :page
|
19
19
|
has_many :menus, through: :menu_items
|
20
20
|
has_many :menus_of_parent, through: :parent, source: :menus
|
21
|
-
has_one :page_menu, foreign_key: :
|
21
|
+
has_one :page_menu, foreign_key: :start_page_id, class_name: "PandaCms::Menu"
|
22
22
|
|
23
23
|
validates :title, presence: true
|
24
|
+
|
24
25
|
validates :path,
|
25
26
|
presence: true,
|
26
27
|
uniqueness: true,
|
27
28
|
format: {with: /\A\/.*\z/, message: "must start with a forward slash"}
|
28
|
-
|
29
|
-
validates :
|
29
|
+
|
30
|
+
validates :parent,
|
31
|
+
presence: true,
|
32
|
+
unless: -> { path == "/" }
|
33
|
+
|
34
|
+
validates :panda_cms_template_id,
|
35
|
+
presence: true
|
30
36
|
|
31
37
|
scope :ordered, -> { order(:lft) }
|
32
38
|
|
@@ -24,9 +24,11 @@ module PandaCms
|
|
24
24
|
validate :validate_template_file_exists
|
25
25
|
|
26
26
|
# Scopes
|
27
|
-
scope :ordered, -> { order(:sort_order) }
|
28
27
|
scope :available, -> { where("max_uses IS NULL OR (pages_count < max_uses)") }
|
29
|
-
|
28
|
+
|
29
|
+
def self.default
|
30
|
+
find_by(file_path: "layouts/page") || first
|
31
|
+
end
|
30
32
|
|
31
33
|
# Generate missing blocks for all templates
|
32
34
|
# @return [void]
|
@@ -2,7 +2,9 @@ module PandaCms
|
|
2
2
|
class User < ApplicationRecord
|
3
3
|
validates :firstname, presence: true
|
4
4
|
validates :lastname, presence: true
|
5
|
-
validates :email, presence: true, uniqueness:
|
5
|
+
validates :email, presence: true, uniqueness: true
|
6
|
+
|
7
|
+
before_save :downcase_email
|
6
8
|
|
7
9
|
def is_admin?
|
8
10
|
admin
|
@@ -15,5 +17,11 @@ module PandaCms
|
|
15
17
|
def self.for_select_list(scope = :all, order = {firstname: :asc, lastname: :asc})
|
16
18
|
PandaCms::User.send(scope).order(order).map { |u| [u.name, u.id] }
|
17
19
|
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def downcase_email
|
24
|
+
self.email = email.to_s.downcase
|
25
|
+
end
|
18
26
|
end
|
19
27
|
end
|
@@ -1,10 +1,12 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
<%
|
1
|
+
<div class="" data-controller="dashboard">
|
2
|
+
<%= render PandaCms::Admin::ContainerComponent.new do |container| %>
|
3
|
+
<% container.with_heading(text: "Dashboard", level: 1) do |heading| %>
|
4
|
+
<% heading.with_button(action: :add, text: "Add Page", link: new_admin_page_path) %>
|
5
|
+
<% end %>
|
6
|
+
<dl class="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-3">
|
7
|
+
<%= render PandaCms::Admin::StatisticsComponent.new(metric: "Views Today", value: PandaCms::Visit.group_by_day(:visited_at, last: 1).count.values.first) %>
|
8
|
+
<%= render PandaCms::Admin::StatisticsComponent.new(metric: "Views Last Week", value: PandaCms::Visit.group_by_week(:visited_at, last: 1).count.values.first) %>
|
9
|
+
<%= render PandaCms::Admin::StatisticsComponent.new(metric: "Views Last Month", value: PandaCms::Visit.group_by_month(:visited_at, last: 1).count.values.first) %>
|
10
|
+
</dl>
|
4
11
|
<% end %>
|
5
|
-
|
6
|
-
<%= render PandaCms::Admin::StatisticsComponent.new(metric: "Views Today", value: PandaCms::Visit.group_by_day(:visited_at, last: 1).count.values.first) %>
|
7
|
-
<%= render PandaCms::Admin::StatisticsComponent.new(metric: "Views Last Week", value: PandaCms::Visit.group_by_week(:visited_at, last: 1).count.values.first) %>
|
8
|
-
<%= render PandaCms::Admin::StatisticsComponent.new(metric: "Views Last Month", value: PandaCms::Visit.group_by_month(:visited_at, last: 1).count.values.first) %>
|
9
|
-
</dl>
|
10
|
-
<% end %>
|
12
|
+
</div>
|
@@ -1,14 +1,13 @@
|
|
1
1
|
<%= render PandaCms::Admin::ContainerComponent.new do |component| %>
|
2
2
|
<% component.with_heading(text: "Add Page", level: 1) do |heading| %>
|
3
3
|
<% end %>
|
4
|
-
|
5
4
|
<%= panda_cms_form_with model: page, url: admin_pages_path, method: :post do |f| %>
|
6
|
-
|
7
|
-
<div data-controller="
|
8
|
-
<input type="hidden" value="<%= PandaCms::Current.root %>" data-
|
9
|
-
<%= f.select :parent_id, options, {}, { "data-
|
10
|
-
<%= f.text_field :title, { data: { "
|
11
|
-
<%= f.text_field :path, { data: { prefix: PandaCms::Current.root, "
|
5
|
+
<% options = nested_set_options(PandaCms::Page, page) { |i| "#{"-" * i.level} #{i.title} (#{i.path})" } %>
|
6
|
+
<div data-controller="slug">
|
7
|
+
<input type="hidden" value="<%= PandaCms::Current.root %>" data-slug-target="existing_root">
|
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, { data: { prefix: PandaCms::Current.root, "slug-target": "output_text" } } %>
|
12
11
|
<%= f.collection_select :panda_cms_template_id, PandaCms::Template.available, :id, :name %>
|
13
12
|
<%= f.button %>
|
14
13
|
</div>
|
@@ -1,8 +1,6 @@
|
|
1
1
|
<%= render PandaCms::Admin::ContainerComponent.new do |component| %>
|
2
2
|
<% component.with_heading(text: "Menus", level: 1) do |heading| %>
|
3
|
-
<% heading.with_button(action: :add, text: "Add Menu", link: new_admin_menu_path) %>
|
4
3
|
<% end %>
|
5
|
-
|
6
4
|
<%= render PandaCms::Admin::TableComponent.new(term: "menu", rows: menus) do |table| %>
|
7
5
|
<% table.column("Name") { |menu| link_to menu.name, edit_admin_menu_path(menu) } %>
|
8
6
|
<% table.column("Kind") { |menu| render PandaCms::Admin::TagComponent.new(status: :active, text: menu.kind.titleize) } %>
|
@@ -1,26 +1,29 @@
|
|
1
1
|
<%= render PandaCms::Admin::ContainerComponent.new do |component| %>
|
2
2
|
<% component.with_heading(text: "#{page.title}", level: 1) %>
|
3
|
-
|
4
3
|
<% component.with_slideover(title: "Page Details") do %>
|
5
4
|
<%= panda_cms_form_with model: page, url: admin_page_path, method: :put do |f| %>
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
<%= f.select :status, options_for_select([["Active", "active"], ["Draft", "draft"], ["Hidden", "hidden"], ["Archived", "archived"]], selected: page.status), {}, class: "block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-mid focus:ring-1 focus:ring-inset focus:ring-dark sm:text-sm sm:leading-6 hover:pointer" %>
|
11
|
-
|
12
|
-
<%= f.submit "Save" %>
|
5
|
+
<%= f.text_field :title, 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:text-sm sm:leading-6 hover:pointer" %>
|
6
|
+
<%= f.text_field :template, value: template.name, readonly: true, class: "read-only:bg-gray-100 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:text-sm sm:leading-6 hover:pointer" %>
|
7
|
+
<%= f.select :status, options_for_select([["Active", "active"], ["Draft", "draft"], ["Hidden", "hidden"], ["Archived", "archived"]], selected: page.status), {}, class: "block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-mid focus:ring-1 focus:ring-inset focus:ring-dark sm:text-sm sm:leading-6 hover:pointer" %>
|
8
|
+
<%= f.submit "Save" %>
|
13
9
|
<% end %>
|
14
10
|
<% end %>
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
11
|
+
<div class="grid grid-cols-2 mb-4 -mt-5">
|
12
|
+
<div>
|
13
|
+
<a class="inline-block mb-2 text-sm text-black/60" target="_blank" href="<%= @page.path %>"><%= @page.path %> <i class="ml-2 fa-solid fa-arrow-up-right-from-square"></i></a>
|
14
|
+
</div>
|
15
|
+
<div class="relative -mt-5">
|
16
|
+
<span class="absolute right-0"><%= render PandaCms::Admin::ButtonComponent.new(text: "Save Changes", action: :save_inactive, icon: "check", link: "#", size: :regular, id: "saveEditableButton") %></span>
|
17
|
+
</div>
|
18
|
+
</div>
|
19
|
+
<%= content_tag :iframe, nil,
|
20
|
+
src: "#{page.path}?embed_id=#{page.id}",
|
21
|
+
class: "p-0 m-0 w-full h-full border border-slate-200",
|
22
|
+
id: "editablePageFrame",
|
23
|
+
data: {
|
24
|
+
controller: "editor",
|
25
|
+
editor_page_id_value: @page.id,
|
26
|
+
editor_admin_path_value: "#{admin_dashboard_url}",
|
27
|
+
editor_autosave_value: false
|
28
|
+
} %>
|
26
29
|
<% end %>
|
@@ -1,14 +1,13 @@
|
|
1
1
|
<%= render PandaCms::Admin::ContainerComponent.new do |component| %>
|
2
2
|
<% component.with_heading(text: "Add Page", level: 1) do |heading| %>
|
3
3
|
<% end %>
|
4
|
-
|
5
4
|
<%= panda_cms_form_with model: page, url: admin_pages_path, method: :post do |f| %>
|
6
|
-
|
7
|
-
<div data-controller="
|
8
|
-
<input type="hidden" value="<%= PandaCms::Current.root %>" data-
|
9
|
-
<%= f.select :parent_id, options, {}, { "data-
|
10
|
-
<%= f.text_field :title, { data: { "
|
11
|
-
<%= f.text_field :path, { meta: t(".path.meta"), data: { prefix: PandaCms::Current.root, "
|
5
|
+
<% options = nested_set_options(PandaCms::Page, page) { |i| "#{"-" * i.level} #{i.title} (#{i.path})" } %>
|
6
|
+
<div data-controller="slug">
|
7
|
+
<input type="hidden" value="<%= PandaCms::Current.root %>" data-slug-target="existing_root">
|
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: PandaCms::Current.root, "slug-target": "output_text" } } %>
|
12
11
|
<%= f.collection_select :panda_cms_template_id, PandaCms::Template.available, :id, :name %>
|
13
12
|
<%= f.button %>
|
14
13
|
</div>
|
@@ -1,8 +1,8 @@
|
|
1
1
|
<%= panda_cms_form_with model: post, url: url do |f| %>
|
2
|
-
<div data-controller="
|
3
|
-
<input type="hidden" value="<%= PandaCms::Current.root %>" data-
|
4
|
-
<%= f.text_field :title, { required: true, data: { "
|
5
|
-
<%= f.text_field :slug, { required: true, data: { prefix: PandaCms::Current.root + "/#{PandaCms.posts[:prefix]}", "
|
2
|
+
<div data-controller="slug">
|
3
|
+
<input type="hidden" value="<%= PandaCms::Current.root %>" data-slug-target="existing_root">
|
4
|
+
<%= f.text_field :title, { required: true, data: { "slug-target": "input_text", action: "focusout->slug#generatePath" } } %>
|
5
|
+
<%= f.text_field :slug, { required: true, data: { prefix: PandaCms::Current.root + "/#{PandaCms.config.posts[:prefix]}", "slug-target": "output_text" } } %>
|
6
6
|
<%= f.select :user_id, PandaCms::User.for_select_list %>
|
7
7
|
<%= f.datetime_field :published_at, { required: true } %>
|
8
8
|
<%= f.select :status, PandaCms::Post.statuses.keys.map { |status| [status.humanize, status] } %>
|
@@ -3,10 +3,9 @@
|
|
3
3
|
<img src="/panda-cms-assets/panda-nav.png" class="py-2 mx-auto w-auto h-32">
|
4
4
|
<h2 class="mt-10 mb-6 text-2xl font-bold text-center text-white"><%= t("panda_cms.admin.sessions.new.title") %></h2>
|
5
5
|
</div>
|
6
|
-
|
7
6
|
<% @providers.each do |provider| %>
|
8
7
|
<div class="mt-4 text-center sm:mx-auto sm:w-full sm:max-w-sm">
|
9
|
-
<%= form_tag "#{PandaCms.
|
8
|
+
<%= form_tag "#{PandaCms.root_path}/auth/#{provider}", method: "post", data: {turbo: false} do %>
|
10
9
|
<input type="hidden" name="redirect_uri" value="<%= admin_login_callback_url(provider: provider) %>">
|
11
10
|
<button type="submit" id="button-sign-in-<%= provider %>" class="inline-flex gap-x-2 items-center py-2.5 px-3.5 mx-auto mb-4 bg-white rounded-md border min-w-56 border-neutral-400">
|
12
11
|
<i class="fa-brands fa-<%= provider %> text-xl mr-1"></i>
|