panda-cms 0.7.2 → 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.
- checksums.yaml +4 -4
- data/README.md +4 -4
- data/app/assets/builds/panda.cms.css +2 -6
- data/app/assets/tailwind/application.css +178 -0
- data/app/assets/tailwind/tailwind.config.js +15 -0
- data/app/builders/panda/cms/form_builder.rb +15 -32
- data/app/components/panda/cms/admin/flash_message_component.html.erb +2 -2
- data/app/components/panda/cms/admin/table_component.html.erb +0 -3
- data/app/components/panda/cms/admin/user_activity_component.rb +7 -18
- data/app/controllers/panda/cms/admin/block_contents_controller.rb +0 -1
- data/app/controllers/panda/cms/admin/forms_controller.rb +0 -1
- data/app/controllers/panda/cms/admin/my_profile_controller.rb +43 -0
- data/app/controllers/panda/cms/admin/pages_controller.rb +14 -4
- data/app/controllers/panda/cms/admin/posts_controller.rb +7 -22
- data/app/controllers/panda/cms/form_submissions_controller.rb +2 -0
- data/app/controllers/panda/cms/pages_controller.rb +10 -8
- data/app/javascript/panda/cms/controllers/index.js +4 -2
- data/app/javascript/panda/cms/controllers/slug_controller.js +64 -31
- data/app/javascript/panda/cms/controllers/theme_form_controller.js +9 -0
- data/app/jobs/panda/cms/record_visit_job.rb +12 -14
- data/app/models/panda/cms/application_record.rb +1 -0
- data/app/models/panda/cms/block.rb +9 -17
- data/app/models/panda/cms/block_content.rb +5 -6
- data/app/models/panda/cms/page.rb +24 -11
- data/app/models/panda/cms/post.rb +3 -5
- data/app/models/panda/cms/redirect.rb +5 -0
- data/app/models/panda/cms/template.rb +6 -7
- data/app/models/panda/cms/visit.rb +1 -1
- data/app/models/panda/social/instagram_post.rb +15 -0
- data/app/services/panda/cms/html_to_editor_js_converter.rb +6 -13
- data/app/services/panda/social/instagram_feed_service.rb +61 -0
- data/app/views/panda/cms/admin/my_profile/edit.html.erb +35 -0
- data/app/views/panda/cms/admin/pages/index.html.erb +1 -1
- data/app/views/panda/cms/admin/pages/new.html.erb +3 -3
- data/app/views/panda/cms/admin/posts/_form.html.erb +10 -0
- data/app/views/panda/cms/admin/posts/edit.html.erb +3 -2
- data/app/views/panda/cms/admin/posts/index.html.erb +1 -1
- data/app/views/panda/cms/admin/settings/index.html.erb +2 -0
- data/app/views/panda/cms/admin/shared/_sidebar.html.erb +1 -1
- data/app/views/panda/cms/shared/_header.html.erb +4 -2
- data/app/views/panda/cms/shared/_importmap.html.erb +1 -0
- data/config/locales/en.yml +5 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20250120235542_remove_paper_trail.rb +55 -0
- data/db/migrate/20250126234001_create_panda_social_instagram_posts.rb +14 -0
- data/db/migrate/20250504221812_add_current_theme_to_panda_cms_users.rb +5 -0
- data/lib/panda/cms/demo_site_generator.rb +25 -4
- data/lib/panda/cms/editor_js_content.rb +21 -0
- data/lib/panda/cms/engine.rb +7 -12
- data/lib/panda-cms/version.rb +1 -1
- data/lib/panda-cms.rb +18 -11
- data/lib/tasks/panda/cms/install.rake +23 -0
- data/lib/tasks/panda/social/instagram.rake +18 -0
- data/public/panda-cms-assets/editor-js/core/editorjs.min.js +83 -0
- data/public/panda-cms-assets/editor-js/plugins/embed.min.js +2 -0
- data/public/panda-cms-assets/editor-js/plugins/header.min.js +9 -0
- data/public/panda-cms-assets/editor-js/plugins/nested-list.min.js +2 -0
- data/public/panda-cms-assets/editor-js/plugins/paragraph.min.js +9 -0
- data/public/panda-cms-assets/editor-js/plugins/quote.min.js +2 -0
- data/public/panda-cms-assets/editor-js/plugins/simple-image.min.js +2 -0
- data/public/panda-cms-assets/editor-js/plugins/table.min.js +2 -0
- metadata +35 -304
- data/app/models/action_text/rich_text_version.rb +0 -6
- data/app/models/panda/cms/block_content_version.rb +0 -8
- data/app/models/panda/cms/page_version.rb +0 -8
- data/app/models/panda/cms/post_version.rb +0 -8
- data/app/models/panda/cms/template_version.rb +0 -8
- data/app/models/panda/cms/version.rb +0 -8
- data/config/initializers/panda/cms/paper_trail.rb +0 -7
- data/db/migrate/20240904200605_create_action_text_tables.action_text.rb +0 -24
- 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
|
-
//
|
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
|
-
|
25
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
53
|
-
|
54
|
-
//
|
55
|
-
|
56
|
-
|
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
|
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
|
}
|
@@ -4,26 +4,24 @@ module Panda
|
|
4
4
|
queue_as :default
|
5
5
|
|
6
6
|
def perform(
|
7
|
-
|
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
|
-
|
12
|
-
|
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:
|
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
|
-
|
23
|
-
|
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
|
@@ -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"
|
7
|
-
has_many :block_contents, foreign_key: :panda_cms_block_id, class_name: "Panda::CMS::BlockContent",
|
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
|
-
|
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
|
-
|
9
|
-
|
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
|
-
|
11
|
-
|
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
|
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 (
|
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: :
|
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
|
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
|
-
|
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(
|
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,
|
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:
|
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:
|
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(
|
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
|
-
<%=
|
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
|
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
|
}
|