panda-cms 0.10.0 → 0.10.3
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 +79 -11
- data/app/assets/tailwind/panda/cms/_application.css +1 -0
- data/app/components/panda/cms/admin/popular_pages_component.rb +62 -0
- data/app/components/panda/cms/code_component.rb +46 -9
- data/app/components/panda/cms/menu_component.rb +18 -5
- data/app/components/panda/cms/page_menu_component.rb +9 -1
- data/app/components/panda/cms/rich_text_component.rb +49 -17
- data/app/components/panda/cms/text_component.rb +46 -14
- data/app/controllers/panda/cms/admin/menus_controller.rb +2 -2
- data/app/controllers/panda/cms/admin/pages_controller.rb +6 -2
- data/app/controllers/panda/cms/admin/posts_controller.rb +3 -1
- data/app/controllers/panda/cms/form_submissions_controller.rb +134 -11
- data/app/controllers/panda/cms/pages_controller.rb +7 -2
- data/app/controllers/panda/cms/posts_controller.rb +16 -0
- data/app/helpers/panda/cms/application_helper.rb +2 -3
- data/app/helpers/panda/cms/asset_helper.rb +14 -72
- data/app/helpers/panda/cms/forms_helper.rb +60 -0
- data/app/helpers/panda/cms/seo_helper.rb +85 -0
- data/app/javascript/panda/cms/{application_panda_cms.js → application.js} +4 -0
- data/app/javascript/panda/cms/controllers/editor_form_controller.js +3 -3
- data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +35 -8
- data/app/javascript/panda/cms/controllers/file_upload_controller.js +165 -0
- data/app/javascript/panda/cms/controllers/index.js +6 -0
- data/app/javascript/panda/cms/controllers/menu_form_controller.js +14 -1
- data/app/javascript/panda/cms/controllers/page_form_controller.js +454 -0
- data/app/javascript/panda/cms/stimulus-loading.js +2 -1
- data/app/jobs/panda/cms/record_visit_job.rb +2 -1
- data/app/models/panda/cms/menu.rb +12 -0
- data/app/models/panda/cms/page.rb +106 -0
- data/app/models/panda/cms/post.rb +97 -0
- data/app/models/panda/cms/visit.rb +16 -1
- data/app/services/panda/social/instagram_feed_service.rb +54 -54
- data/app/views/layouts/homepage.html.erb +1 -4
- data/app/views/layouts/page.html.erb +1 -4
- data/app/views/panda/cms/admin/dashboard/show.html.erb +11 -4
- data/app/views/panda/cms/admin/files/index.html.erb +1 -1
- data/app/views/panda/cms/admin/forms/new.html.erb +1 -1
- data/app/views/panda/cms/admin/forms/show.html.erb +3 -3
- data/app/views/panda/cms/admin/menus/_menu_item_fields.html.erb +3 -3
- data/app/views/panda/cms/admin/menus/edit.html.erb +12 -14
- data/app/views/panda/cms/admin/menus/index.html.erb +1 -1
- data/app/views/panda/cms/admin/menus/new.html.erb +6 -8
- data/app/views/panda/cms/admin/pages/edit.html.erb +213 -20
- data/app/views/panda/cms/admin/pages/index.html.erb +6 -6
- data/app/views/panda/cms/admin/posts/_form.html.erb +47 -8
- data/app/views/panda/cms/admin/posts/edit.html.erb +1 -1
- data/app/views/panda/cms/admin/posts/index.html.erb +4 -4
- data/app/views/panda/cms/shared/_favicons.html.erb +7 -7
- data/app/views/shared/_header.html.erb +1 -4
- data/config/brakeman.ignore +38 -0
- data/config/importmap.rb +7 -6
- data/config/initializers/groupdate.rb +5 -0
- data/config/locales/en.yml +42 -2
- data/config/routes.rb +1 -1
- data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +0 -10
- data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +0 -6
- data/db/migrate/20240317230622_create_panda_cms_visits.rb +1 -1
- data/db/migrate/20240805121123_create_panda_cms_posts.rb +1 -1
- data/db/migrate/20240806112735_fix_panda_cms_visits_column_names.rb +1 -1
- data/db/migrate/20240923234535_add_depth_to_panda_cms_menus.rb +0 -6
- data/db/migrate/20250106223303_add_author_id_to_panda_cms_posts.rb +1 -3
- data/db/migrate/20251109131150_add_seo_fields_to_pages.rb +32 -0
- data/db/migrate/20251109131205_add_seo_fields_to_posts.rb +27 -0
- data/db/migrate/20251110114258_add_spam_tracking_to_form_submissions.rb +7 -0
- data/db/migrate/20251110122812_add_performance_indexes_to_pages_and_redirects.rb +13 -0
- data/db/migrate/20251117234530_add_index_to_visited_at_on_panda_cms_visits.rb +7 -0
- data/db/migrate/20251118015100_backfill_visited_at_for_existing_visits.rb +17 -0
- data/db/seeds.rb +5 -0
- data/lib/panda/cms/asset_loader.rb +42 -78
- data/lib/panda/cms/bulk_editor.rb +288 -12
- data/lib/panda/cms/engine/asset_config.rb +49 -0
- data/lib/panda/cms/engine/autoload_config.rb +37 -0
- data/lib/panda/cms/engine/backtrace_config.rb +42 -0
- data/lib/panda/cms/engine/core_config.rb +106 -0
- data/lib/panda/cms/engine/helper_config.rb +20 -0
- data/lib/panda/cms/engine/route_config.rb +33 -0
- data/lib/panda/cms/engine/view_component_config.rb +31 -0
- data/lib/panda/cms/engine.rb +32 -228
- data/lib/{panda-cms → panda/cms}/version.rb +1 -1
- data/lib/panda/cms.rb +12 -0
- data/lib/panda-cms.rb +24 -3
- data/lib/tasks/ci.rake +0 -0
- metadata +32 -67
- data/app/assets/builds/panda.cms.css +0 -2754
- data/app/assets/stylesheets/panda/cms/application.tailwind.css +0 -162
- data/app/assets/stylesheets/panda/cms/editor.css +0 -120
- data/app/assets/tailwind/application.css +0 -178
- data/app/assets/tailwind/tailwind.config.js +0 -15
- data/app/javascript/panda_cms/stimulus-loading.js +0 -39
- data/app/views/panda/cms/shared/_importmap.html.erb +0 -34
- data/config/initializers/inflections.rb +0 -5
- data/config/initializers/panda/cms/healthcheck_log_silencer.rb.disabled +0 -31
- data/db/migrate/20240317010532_create_panda_cms_users.rb +0 -14
- data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +0 -61
- data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +0 -7
- data/db/migrate/20240701225422_add_service_name_to_active_storage_blobs.active_storage.rb +0 -24
- data/db/migrate/20240701225423_create_active_storage_variant_records.active_storage.rb +0 -30
- data/db/migrate/20240701225424_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +0 -10
- data/db/migrate/20241119214548_convert_post_content_to_editor_js.rb +0 -37
- data/db/migrate/20250809231125_migrate_users_to_panda_core.rb +0 -113
- data/lib/generators/panda/cms/install_generator.rb +0 -28
- data/lib/tasks/assets.rake +0 -540
- data/public/panda-cms-assets/editor-js/core/editorjs.min.js +0 -83
- data/public/panda-cms-assets/editor-js/plugins/embed.min.js +0 -2
- data/public/panda-cms-assets/editor-js/plugins/header.min.js +0 -9
- data/public/panda-cms-assets/editor-js/plugins/nested-list.min.js +0 -2
- data/public/panda-cms-assets/editor-js/plugins/paragraph.min.js +0 -9
- data/public/panda-cms-assets/editor-js/plugins/quote.min.js +0 -2
- data/public/panda-cms-assets/editor-js/plugins/simple-image.min.js +0 -2
- data/public/panda-cms-assets/editor-js/plugins/table.min.js +0 -2
- data/public/panda-cms-assets/favicons/android-chrome-192x192.png +0 -0
- data/public/panda-cms-assets/favicons/android-chrome-512x512.png +0 -0
- data/public/panda-cms-assets/favicons/apple-touch-icon.png +0 -0
- data/public/panda-cms-assets/favicons/browserconfig.xml +0 -9
- data/public/panda-cms-assets/favicons/favicon-16x16.png +0 -0
- data/public/panda-cms-assets/favicons/favicon-32x32.png +0 -0
- data/public/panda-cms-assets/favicons/favicon.ico +0 -0
- data/public/panda-cms-assets/favicons/mstile-150x150.png +0 -0
- data/public/panda-cms-assets/favicons/safari-pinned-tab.svg +0 -61
- data/public/panda-cms-assets/favicons/site.webmanifest +0 -14
- data/public/panda-cms-assets/manifest.json +0 -20
- data/public/panda-cms-assets/panda-cms-0.7.4.css +0 -26
- data/public/panda-cms-assets/panda-cms-0.7.4.js +0 -150
- data/public/panda-cms-assets/panda-logo-screenprint.png +0 -0
- data/public/panda-cms-assets/panda-nav.png +0 -0
- data/public/panda-cms-assets/rich_text_editor.css +0 -568
- /data/db/migrate/{20251105000001_add_pending_review_status_to_pages_and_posts.panda_cms.rb → 20251105000001_add_pending_review_status_to_pages_and_posts.rb} +0 -0
|
@@ -6,6 +6,7 @@ module Panda
|
|
|
6
6
|
self.table_name = "panda_cms_menus"
|
|
7
7
|
|
|
8
8
|
after_save :generate_auto_menu_items, if: -> { kind == "auto" }
|
|
9
|
+
after_commit :clear_menu_cache
|
|
9
10
|
|
|
10
11
|
has_many :menu_items, lambda {
|
|
11
12
|
order(lft: :asc)
|
|
@@ -50,6 +51,17 @@ module Panda
|
|
|
50
51
|
|
|
51
52
|
errors.add(:start_page, "can't be blank")
|
|
52
53
|
end
|
|
54
|
+
|
|
55
|
+
#
|
|
56
|
+
# Clear fragment cache when menu is updated
|
|
57
|
+
# This ensures menu changes appear immediately on the front-end
|
|
58
|
+
#
|
|
59
|
+
# @return nil
|
|
60
|
+
# @visibility private
|
|
61
|
+
#
|
|
62
|
+
def clear_menu_cache
|
|
63
|
+
Rails.cache.delete("panda_cms_menu/#{name}/#{id}")
|
|
64
|
+
end
|
|
53
65
|
end
|
|
54
66
|
end
|
|
55
67
|
end
|
|
@@ -51,6 +51,31 @@ module Panda
|
|
|
51
51
|
code: "code"
|
|
52
52
|
}, prefix: :type
|
|
53
53
|
|
|
54
|
+
enum :seo_index_mode, {
|
|
55
|
+
visible: "visible",
|
|
56
|
+
invisible: "invisible"
|
|
57
|
+
}, prefix: :seo
|
|
58
|
+
|
|
59
|
+
enum :og_type, {
|
|
60
|
+
website: "website",
|
|
61
|
+
article: "article",
|
|
62
|
+
profile: "profile",
|
|
63
|
+
video: "video",
|
|
64
|
+
book: "book"
|
|
65
|
+
}, prefix: :og
|
|
66
|
+
|
|
67
|
+
# Active Storage attachment for Open Graph image
|
|
68
|
+
has_one_attached :og_image do |attachable|
|
|
69
|
+
attachable.variant :og_share, resize_to_limit: [1200, 630]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# SEO validations
|
|
73
|
+
validates :seo_title, length: {maximum: 70}, allow_blank: true
|
|
74
|
+
validates :seo_description, length: {maximum: 160}, allow_blank: true
|
|
75
|
+
validates :og_title, length: {maximum: 60}, allow_blank: true
|
|
76
|
+
validates :og_description, length: {maximum: 200}, allow_blank: true
|
|
77
|
+
validates :canonical_url, format: {with: URI::DEFAULT_PARSER.make_regexp(%w[http https])}, allow_blank: true
|
|
78
|
+
|
|
54
79
|
# Callbacks
|
|
55
80
|
after_save :handle_after_save
|
|
56
81
|
before_save :update_cached_last_updated_at
|
|
@@ -91,6 +116,85 @@ module Panda
|
|
|
91
116
|
new_timestamp
|
|
92
117
|
end
|
|
93
118
|
|
|
119
|
+
#
|
|
120
|
+
# Returns the effective SEO title for this page
|
|
121
|
+
# Falls back to page title if not set, with optional inheritance
|
|
122
|
+
#
|
|
123
|
+
# @return [String] The SEO title to use
|
|
124
|
+
# @visibility public
|
|
125
|
+
#
|
|
126
|
+
def effective_seo_title
|
|
127
|
+
return seo_title if seo_title.present?
|
|
128
|
+
return title unless inherit_seo
|
|
129
|
+
|
|
130
|
+
# Traverse up tree to find inherited value
|
|
131
|
+
self_and_ancestors.reverse.find { |p| p.seo_title.present? }&.seo_title || title
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
#
|
|
135
|
+
# Returns the effective SEO description for this page
|
|
136
|
+
# With optional inheritance from parent pages
|
|
137
|
+
#
|
|
138
|
+
# @return [String, nil] The SEO description to use
|
|
139
|
+
# @visibility public
|
|
140
|
+
#
|
|
141
|
+
def effective_seo_description
|
|
142
|
+
return seo_description if seo_description.present?
|
|
143
|
+
return nil unless inherit_seo
|
|
144
|
+
|
|
145
|
+
self_and_ancestors.reverse.find { |p| p.seo_description.present? }&.seo_description
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
#
|
|
149
|
+
# Returns the effective Open Graph title
|
|
150
|
+
# Falls back to SEO title, then page title
|
|
151
|
+
#
|
|
152
|
+
# @return [String] The OG title to use
|
|
153
|
+
# @visibility public
|
|
154
|
+
#
|
|
155
|
+
def effective_og_title
|
|
156
|
+
og_title.presence || effective_seo_title
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
#
|
|
160
|
+
# Returns the effective Open Graph description
|
|
161
|
+
# Falls back to SEO description
|
|
162
|
+
#
|
|
163
|
+
# @return [String, nil] The OG description to use
|
|
164
|
+
# @visibility public
|
|
165
|
+
#
|
|
166
|
+
def effective_og_description
|
|
167
|
+
og_description.presence || effective_seo_description
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
#
|
|
171
|
+
# Returns the effective canonical URL for this page
|
|
172
|
+
# Falls back to the page's own URL if not explicitly set
|
|
173
|
+
#
|
|
174
|
+
# @return [String] The canonical URL to use
|
|
175
|
+
# @visibility public
|
|
176
|
+
#
|
|
177
|
+
def effective_canonical_url
|
|
178
|
+
canonical_url.presence || path
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
#
|
|
182
|
+
# Generates the robots meta tag content based on seo_index_mode
|
|
183
|
+
#
|
|
184
|
+
# @return [String] The robots meta tag content (e.g., "index, follow")
|
|
185
|
+
# @visibility public
|
|
186
|
+
#
|
|
187
|
+
def robots_meta_content
|
|
188
|
+
case seo_index_mode
|
|
189
|
+
when "visible"
|
|
190
|
+
"index, follow"
|
|
191
|
+
when "invisible"
|
|
192
|
+
"noindex, nofollow"
|
|
193
|
+
else
|
|
194
|
+
"index, follow" # Default fallback
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
94
198
|
private
|
|
95
199
|
|
|
96
200
|
def validate_unique_path_in_scope
|
|
@@ -163,6 +267,8 @@ module Panda
|
|
|
163
267
|
def update_cached_last_updated_at
|
|
164
268
|
# Will be set to updated_at automatically during save
|
|
165
269
|
# Block content updates will call refresh_last_updated_at! separately
|
|
270
|
+
# Only update if column exists (for backwards compatibility with older schemas)
|
|
271
|
+
return unless self.class.column_names.include?("cached_last_updated_at")
|
|
166
272
|
self.cached_last_updated_at = Time.current
|
|
167
273
|
end
|
|
168
274
|
end
|
|
@@ -38,6 +38,31 @@ module Panda
|
|
|
38
38
|
archived: "archived"
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
enum :seo_index_mode, {
|
|
42
|
+
visible: "visible",
|
|
43
|
+
invisible: "invisible"
|
|
44
|
+
}, prefix: :seo
|
|
45
|
+
|
|
46
|
+
enum :og_type, {
|
|
47
|
+
website: "website",
|
|
48
|
+
article: "article",
|
|
49
|
+
profile: "profile",
|
|
50
|
+
video: "video",
|
|
51
|
+
book: "book"
|
|
52
|
+
}, prefix: :og
|
|
53
|
+
|
|
54
|
+
# Active Storage attachment for Open Graph image
|
|
55
|
+
has_one_attached :og_image do |attachable|
|
|
56
|
+
attachable.variant :og_share, resize_to_limit: [1200, 630]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# SEO validations
|
|
60
|
+
validates :seo_title, length: {maximum: 70}, allow_blank: true
|
|
61
|
+
validates :seo_description, length: {maximum: 160}, allow_blank: true
|
|
62
|
+
validates :og_title, length: {maximum: 60}, allow_blank: true
|
|
63
|
+
validates :og_description, length: {maximum: 200}, allow_blank: true
|
|
64
|
+
validates :canonical_url, format: {with: URI::DEFAULT_PARSER.make_regexp(%w[http https])}, allow_blank: true
|
|
65
|
+
|
|
41
66
|
def to_param
|
|
42
67
|
# For date-based URLs, return just the slug portion
|
|
43
68
|
parts = CGI.unescape(slug).delete_prefix("/").split("/")
|
|
@@ -88,6 +113,78 @@ module Panda
|
|
|
88
113
|
text.truncate(length).html_safe
|
|
89
114
|
end
|
|
90
115
|
|
|
116
|
+
#
|
|
117
|
+
# Returns the effective SEO title for this post
|
|
118
|
+
# Falls back to post title if not set
|
|
119
|
+
#
|
|
120
|
+
# @return [String] The SEO title to use
|
|
121
|
+
# @visibility public
|
|
122
|
+
#
|
|
123
|
+
def effective_seo_title
|
|
124
|
+
seo_title.presence || title
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
#
|
|
128
|
+
# Returns the effective SEO description for this post
|
|
129
|
+
# Falls back to excerpt if not set
|
|
130
|
+
#
|
|
131
|
+
# @return [String, nil] The SEO description to use
|
|
132
|
+
# @visibility public
|
|
133
|
+
#
|
|
134
|
+
def effective_seo_description
|
|
135
|
+
seo_description.presence || excerpt(160, squish: true)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
#
|
|
139
|
+
# Returns the effective Open Graph title
|
|
140
|
+
# Falls back to SEO title, then post title
|
|
141
|
+
#
|
|
142
|
+
# @return [String] The OG title to use
|
|
143
|
+
# @visibility public
|
|
144
|
+
#
|
|
145
|
+
def effective_og_title
|
|
146
|
+
og_title.presence || effective_seo_title
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
#
|
|
150
|
+
# Returns the effective Open Graph description
|
|
151
|
+
# Falls back to SEO description or excerpt
|
|
152
|
+
#
|
|
153
|
+
# @return [String, nil] The OG description to use
|
|
154
|
+
# @visibility public
|
|
155
|
+
#
|
|
156
|
+
def effective_og_description
|
|
157
|
+
og_description.presence || effective_seo_description
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
#
|
|
161
|
+
# Returns the effective canonical URL for this post
|
|
162
|
+
# Falls back to the post's own URL if not explicitly set
|
|
163
|
+
#
|
|
164
|
+
# @return [String] The canonical URL to use
|
|
165
|
+
# @visibility public
|
|
166
|
+
#
|
|
167
|
+
def effective_canonical_url
|
|
168
|
+
canonical_url.presence || slug
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
#
|
|
172
|
+
# Generates the robots meta tag content based on seo_index_mode
|
|
173
|
+
#
|
|
174
|
+
# @return [String] The robots meta tag content (e.g., "index, follow")
|
|
175
|
+
# @visibility public
|
|
176
|
+
#
|
|
177
|
+
def robots_meta_content
|
|
178
|
+
case seo_index_mode
|
|
179
|
+
when "visible"
|
|
180
|
+
"index, follow"
|
|
181
|
+
when "invisible"
|
|
182
|
+
"noindex, nofollow"
|
|
183
|
+
else
|
|
184
|
+
"index, follow" # Default fallback
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
91
188
|
private
|
|
92
189
|
|
|
93
190
|
def clear_menu_cache
|
|
@@ -3,9 +3,24 @@
|
|
|
3
3
|
module Panda
|
|
4
4
|
module CMS
|
|
5
5
|
class Visit < ApplicationRecord
|
|
6
|
-
belongs_to :page, class_name: "Panda::CMS::Page", foreign_key: :
|
|
6
|
+
belongs_to :page, class_name: "Panda::CMS::Page", foreign_key: :page_id, optional: true
|
|
7
7
|
belongs_to :user, class_name: "Panda::Core::User", foreign_key: :user_id, optional: true
|
|
8
8
|
belongs_to :redirect, class_name: "Panda::CMS::Redirect", foreign_key: :redirect_id, optional: true
|
|
9
|
+
|
|
10
|
+
# Returns the most popular pages by visit count
|
|
11
|
+
# @param limit [Integer] Number of pages to return (default: 10)
|
|
12
|
+
# @param period [ActiveSupport::Duration] Time period to consider (default: all time)
|
|
13
|
+
# @return [Array<Hash>] Array of hashes with page and visit_count
|
|
14
|
+
def self.popular_pages(limit: 10, period: nil)
|
|
15
|
+
scope = joins(:page).where.not(page_id: nil)
|
|
16
|
+
scope = scope.where("visited_at >= ?", period.ago) if period
|
|
17
|
+
|
|
18
|
+
scope
|
|
19
|
+
.group("panda_cms_pages.id", "panda_cms_pages.title", "panda_cms_pages.path")
|
|
20
|
+
.select("panda_cms_pages.id, panda_cms_pages.title, panda_cms_pages.path, COUNT(*) as visit_count")
|
|
21
|
+
.order("visit_count DESC")
|
|
22
|
+
.limit(limit)
|
|
23
|
+
end
|
|
9
24
|
end
|
|
10
25
|
end
|
|
11
26
|
end
|
|
@@ -1,63 +1,63 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "http"
|
|
4
|
-
require "down"
|
|
3
|
+
# require "http"
|
|
4
|
+
# require "down"
|
|
5
5
|
|
|
6
6
|
module Panda
|
|
7
7
|
module Social
|
|
8
8
|
class InstagramFeedService
|
|
9
|
-
GRAPH_API_VERSION = "v19.0"
|
|
10
|
-
GRAPH_API_BASE_URL = "https://graph.instagram.com/#{GRAPH_API_VERSION}".freeze
|
|
11
|
-
|
|
12
|
-
def initialize(access_token)
|
|
13
|
-
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def sync_recent_posts
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
def fetch_media
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def process_post(post_data)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
rescue Down::Error => e
|
|
57
|
-
|
|
58
|
-
rescue => e
|
|
59
|
-
|
|
60
|
-
end
|
|
9
|
+
# GRAPH_API_VERSION = "v19.0"
|
|
10
|
+
# GRAPH_API_BASE_URL = "https://graph.instagram.com/#{GRAPH_API_VERSION}".freeze
|
|
11
|
+
|
|
12
|
+
# def initialize(access_token)
|
|
13
|
+
# @access_token = access_token
|
|
14
|
+
# end
|
|
15
|
+
|
|
16
|
+
# def sync_recent_posts
|
|
17
|
+
# fetch_media.each do |post_data|
|
|
18
|
+
# process_post(post_data)
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
|
|
22
|
+
# private
|
|
23
|
+
|
|
24
|
+
# def fetch_media
|
|
25
|
+
# response = HTTP.get("#{GRAPH_API_BASE_URL}/me/media", params: {
|
|
26
|
+
# access_token: @access_token,
|
|
27
|
+
# fields: "id,caption,media_type,media_url,permalink,timestamp"
|
|
28
|
+
# })
|
|
29
|
+
|
|
30
|
+
# return [] unless response.status.success?
|
|
31
|
+
|
|
32
|
+
# JSON.parse(response.body.to_s)["data"]
|
|
33
|
+
# end
|
|
34
|
+
|
|
35
|
+
# def process_post(post_data)
|
|
36
|
+
# return unless post_data["media_type"] == "IMAGE"
|
|
37
|
+
|
|
38
|
+
# instagram_post = InstagramPost.find_or_initialize_by(instagram_id: post_data["id"])
|
|
39
|
+
|
|
40
|
+
# instagram_post.assign_attributes(
|
|
41
|
+
# caption: post_data["caption"],
|
|
42
|
+
# posted_at: Time.zone.parse(post_data["timestamp"]),
|
|
43
|
+
# permalink: post_data["permalink"]
|
|
44
|
+
# )
|
|
45
|
+
|
|
46
|
+
# if instagram_post.new_record? || instagram_post.changed?
|
|
47
|
+
# # Download and attach image
|
|
48
|
+
# tempfile = Down.download(post_data["media_url"])
|
|
49
|
+
# instagram_post.image.attach(
|
|
50
|
+
# io: tempfile,
|
|
51
|
+
# filename: File.basename(post_data["media_url"])
|
|
52
|
+
# )
|
|
53
|
+
|
|
54
|
+
# instagram_post.save!
|
|
55
|
+
# end
|
|
56
|
+
# rescue Down::Error => e
|
|
57
|
+
# Rails.logger.error "Failed to download Instagram image: #{e.message}"
|
|
58
|
+
# rescue => e
|
|
59
|
+
# Rails.logger.error "Error processing Instagram post #{post_data["id"]}: #{e.message}"
|
|
60
|
+
# end
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
end
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
4
|
<title>Test Homepage</title>
|
|
5
|
-
|
|
6
|
-
<!-- Include Panda CMS assets for editor functionality when in edit mode -->
|
|
7
|
-
<%= panda_cms_complete_assets %>
|
|
8
|
-
<% end %>
|
|
5
|
+
<!-- CMS assets are automatically injected by editor_iframe_controller when in edit mode -->
|
|
9
6
|
</head>
|
|
10
7
|
<body>
|
|
11
8
|
<h1><%= @page.title %></h1>
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
4
|
<title>Test Page</title>
|
|
5
|
-
|
|
6
|
-
<!-- Include Panda CMS assets for editor functionality when in edit mode -->
|
|
7
|
-
<%= panda_cms_complete_assets %>
|
|
8
|
-
<% end %>
|
|
5
|
+
<!-- CMS assets are automatically injected by editor_iframe_controller when in edit mode -->
|
|
9
6
|
</head>
|
|
10
7
|
<body>
|
|
11
8
|
<h1><%= @page.title %></h1>
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
<div class="" data-controller="dashboard">
|
|
2
2
|
<%= render Panda::Core::Admin::ContainerComponent.new do |container| %>
|
|
3
3
|
<% container.heading(text: "Dashboard", level: 1) do |heading| %>
|
|
4
|
-
<% heading.button(action: :add, text: "Add Page",
|
|
4
|
+
<% heading.button(action: :add, text: "Add Page", href: new_admin_cms_page_path) %>
|
|
5
5
|
<% end %>
|
|
6
6
|
<dl class="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-3">
|
|
7
|
-
<%= render Panda::Core::Admin::StatisticsComponent.new(metric: "Views Today", value: Panda::CMS::Visit.group_by_day(:visited_at, last: 1).count.values.first) %>
|
|
8
|
-
<%= render Panda::Core::Admin::StatisticsComponent.new(metric: "Views Last Week", value: Panda::CMS::Visit.group_by_week(:visited_at, last: 1).count.values.first) %>
|
|
9
|
-
<%= render Panda::Core::Admin::StatisticsComponent.new(metric: "Views Last Month", value: Panda::CMS::Visit.group_by_month(:visited_at, last: 1).count.values.first) %>
|
|
7
|
+
<%= render Panda::Core::Admin::StatisticsComponent.new(metric: "Views Today", value: Panda::CMS::Visit.group_by_day(:visited_at, last: 1).count.values.first || 0) %>
|
|
8
|
+
<%= render Panda::Core::Admin::StatisticsComponent.new(metric: "Views Last Week", value: Panda::CMS::Visit.group_by_week(:visited_at, last: 1).count.values.first || 0) %>
|
|
9
|
+
<%= render Panda::Core::Admin::StatisticsComponent.new(metric: "Views Last Month", value: Panda::CMS::Visit.group_by_month(:visited_at, last: 1).count.values.first || 0) %>
|
|
10
10
|
</dl>
|
|
11
|
+
|
|
12
|
+
<div class="grid grid-cols-1 gap-5 mt-8">
|
|
13
|
+
<%= render Panda::CMS::Admin::PopularPagesComponent.new(
|
|
14
|
+
popular_pages: Panda::CMS::Visit.popular_pages(limit: 10),
|
|
15
|
+
period_name: "All Time"
|
|
16
|
+
) %>
|
|
17
|
+
</div>
|
|
11
18
|
<% end %>
|
|
12
19
|
</div>
|
|
@@ -12,6 +12,6 @@
|
|
|
12
12
|
<% end %>
|
|
13
13
|
|
|
14
14
|
<div data-controller="file-gallery" class="pb-24">
|
|
15
|
-
<%=
|
|
15
|
+
<%= Panda::Core::Admin::FileGalleryComponent.new(files: @files, selected_file: @selected_file) %>
|
|
16
16
|
</div>
|
|
17
17
|
<% end %>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<div data-controller="slug">
|
|
6
6
|
<input type="hidden" value="<%= Panda::CMS::Current.root %>" data-slug-target="existing_root">
|
|
7
7
|
<%= f.select :parent_id, options, {}, { "data-slug-target": "input_select", "data-action": "change->slug#setPrePath" } %>
|
|
8
|
-
<%= f.text_field :title, { data: { "slug-target": "input_text", action: "focusout->slug#generatePath" }
|
|
8
|
+
<%= f.text_field :title, { data: { "slug-target": "input_text" }, "data-action": "focusout->slug#generatePath" } %>
|
|
9
9
|
<%= f.text_field :path, { data: { prefix: Panda::CMS::Current.root, "slug-target": "output_text" } } %>
|
|
10
10
|
<%= f.collection_select :panda_cms_template_id, Panda::CMS::Template.available, :id, :name %>
|
|
11
11
|
<%= f.button %>
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
<%= render Panda::Core::Admin::TableComponent.new(term: "submission", rows: submissions) do |table| %>
|
|
6
6
|
<% fields.each do |field, title| %>
|
|
7
7
|
<% table.column(title) do |submission| %>
|
|
8
|
-
<% if field
|
|
9
|
-
<a href="mailto:<%= submission.data[field
|
|
8
|
+
<% if field == "email" || field == "email_address" %>
|
|
9
|
+
<a href="mailto:<%= submission.data[field] %>" class="border-b border-gray-500 hover:text-gray-900"><%= submission.data[field] %></a>
|
|
10
10
|
<% else %>
|
|
11
|
-
<%= simple_format(submission.data[field
|
|
11
|
+
<%= simple_format(submission.data[field]) %>
|
|
12
12
|
<% end %>
|
|
13
13
|
<% end %>
|
|
14
14
|
<% end %>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<div class="mb-6 pb-6 border-b border-gray-200 dark:border-gray-700 nested-form-wrapper" data-new-record="<%= form.object.new_record? %>">
|
|
2
|
-
<%= form.text_field :text,
|
|
3
|
-
<%= form.collection_select :panda_cms_page_id, Panda::CMS::Page.order(:title), :id, :title, { include_blank: "Select a page (optional)" }, { class: "mt-1" } %>
|
|
4
|
-
<%= form.text_field :external_url,
|
|
2
|
+
<%= form.text_field :text, label: "Menu Item Text" %>
|
|
3
|
+
<%= form.collection_select :panda_cms_page_id, Panda::CMS::Page.order(:title), :id, :title, { include_blank: "Select a page (optional)", label: "Page" }, { class: "mt-1" } %>
|
|
4
|
+
<%= form.text_field :external_url, label: "External URL (optional)" %>
|
|
5
5
|
|
|
6
6
|
<div class="mt-3">
|
|
7
7
|
<%= render Panda::Core::Admin::ButtonComponent.new(text: "Remove", action: :delete, link: "#", size: :small, data: { action: "click->nested-form#remove" }) %>
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
<%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
|
|
2
|
-
<% component.heading(text: "Edit Menu: #{menu.name}", level: 1) %>
|
|
2
|
+
<% component.heading(text: "Edit Menu: #{@menu.name}", level: 1) %>
|
|
3
3
|
|
|
4
|
-
<%= panda_cms_form_with model: menu, url: admin_cms_menu_path(menu), method: :put do |f| %>
|
|
5
|
-
<%= render Panda::Core::Admin::FormErrorComponent.new(model: menu) %>
|
|
4
|
+
<%= panda_cms_form_with model: @menu, url: admin_cms_menu_path(@menu), method: :put, data: { controller: "menu-form" } do |f| %>
|
|
5
|
+
<%= render Panda::Core::Admin::FormErrorComponent.new(model: @menu) %>
|
|
6
6
|
|
|
7
7
|
<%= f.text_field :name %>
|
|
8
|
-
<%= f.select :kind, options_for_select([["Static", "static"], ["Auto", "auto"]], selected: menu.kind), {}, { data
|
|
8
|
+
<%= f.select :kind, options_for_select([["Static", "static"], ["Auto", "auto"]], selected: @menu.kind), {}, { "data-action": "change->menu-form#kindChanged" } %>
|
|
9
9
|
|
|
10
|
-
<div data-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
</div>
|
|
10
|
+
<div data-menu-form-target="startPageField" class="<%= 'hidden' unless @menu.kind == 'auto' %>">
|
|
11
|
+
<%= f.collection_select :start_page_id, Panda::CMS::Page.order(:title), :id, :title, { include_blank: "Select a page...", label: "Start Page" }, { class: "mt-1" } %>
|
|
12
|
+
</div>
|
|
14
13
|
|
|
15
|
-
<% if menu.kind == "static" %>
|
|
14
|
+
<% if @menu.kind == "static" %>
|
|
16
15
|
<%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
|
|
17
16
|
<% panel.heading(text: "Menu Items") %>
|
|
18
17
|
|
|
@@ -24,8 +23,8 @@
|
|
|
24
23
|
</template>
|
|
25
24
|
|
|
26
25
|
<div class="space-y-4">
|
|
27
|
-
<% if menu.menu_items.any? %>
|
|
28
|
-
<%= f.fields_for :menu_items, menu.menu_items.sort_by(&:lft) do |item_form| %>
|
|
26
|
+
<% if @menu.menu_items.any? %>
|
|
27
|
+
<%= f.fields_for :menu_items, @menu.menu_items.sort_by(&:lft) do |item_form| %>
|
|
29
28
|
<%= render "menu_item_fields", form: item_form %>
|
|
30
29
|
<% end %>
|
|
31
30
|
<% end %>
|
|
@@ -42,8 +41,8 @@
|
|
|
42
41
|
<%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
|
|
43
42
|
<% panel.heading(text: "Auto-Generated Menu Items") %>
|
|
44
43
|
|
|
45
|
-
<% if menu.menu_items.any? %>
|
|
46
|
-
<%= render Panda::Core::Admin::TableComponent.new(term: "menu item", rows: menu.menu_items.root.self_and_descendants) do |table| %>
|
|
44
|
+
<% if @menu.menu_items.any? %>
|
|
45
|
+
<%= render Panda::Core::Admin::TableComponent.new(term: "menu item", rows: @menu.menu_items.root.self_and_descendants) do |table| %>
|
|
47
46
|
<% table.column("Text") do |menu_item| %>
|
|
48
47
|
<div class="<%= "ml-#{menu_item.depth * 6}" %>">
|
|
49
48
|
<%= menu_item.text %>
|
|
@@ -57,7 +56,6 @@
|
|
|
57
56
|
<% end %>
|
|
58
57
|
<% end %>
|
|
59
58
|
<% end %>
|
|
60
|
-
</div>
|
|
61
59
|
|
|
62
60
|
<%= f.button "Save Menu" %>
|
|
63
61
|
<% end %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
|
|
2
2
|
<% component.heading(text: "Menus", level: 1) do |heading| %>
|
|
3
|
-
<% heading.button(action: :add, text: "New Menu",
|
|
3
|
+
<% heading.button(action: :add, text: "New Menu", href: new_admin_cms_menu_path) %>
|
|
4
4
|
<% end %>
|
|
5
5
|
<%= render Panda::Core::Admin::TableComponent.new(term: "menu", rows: menus) do |table| %>
|
|
6
6
|
<% table.column("Name") { |menu| link_to menu.name, edit_admin_cms_menu_path(menu) } %>
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
<%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
|
|
2
2
|
<% component.heading(text: "New Menu", level: 1) %>
|
|
3
3
|
|
|
4
|
-
<%= panda_cms_form_with model: menu, url: admin_cms_menus_path, method: :post do |f| %>
|
|
4
|
+
<%= panda_cms_form_with model: menu, url: admin_cms_menus_path, method: :post, data: { controller: "menu-form" } do |f| %>
|
|
5
5
|
<%= render Panda::Core::Admin::FormErrorComponent.new(model: menu) %>
|
|
6
6
|
|
|
7
7
|
<%= f.text_field :name %>
|
|
8
|
-
<%= f.select :kind, options_for_select([["Static", "static"], ["Auto", "auto"]], selected: menu.kind || "static"), {}, { data
|
|
8
|
+
<%= f.select :kind, options_for_select([["Static", "static"], ["Auto", "auto"]], selected: menu.kind || "static"), {}, { "data-action": "change->menu-form#kindChanged" } %>
|
|
9
9
|
|
|
10
|
-
<div data-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
</div>
|
|
10
|
+
<div data-menu-form-target="startPageField" class="hidden">
|
|
11
|
+
<%= f.collection_select :start_page_id, Panda::CMS::Page.order(:title), :id, :title, { include_blank: "Select a page...", label: "Start Page" }, { class: "mt-1" } %>
|
|
12
|
+
</div>
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
<div data-menu-form-target="menuItemsSection">
|
|
16
15
|
<%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
|
|
17
16
|
<% panel.heading(text: "Menu Items") %>
|
|
18
17
|
|
|
@@ -33,7 +32,6 @@
|
|
|
33
32
|
</div>
|
|
34
33
|
<% end %>
|
|
35
34
|
</div>
|
|
36
|
-
</div>
|
|
37
35
|
|
|
38
36
|
<%= f.button "Create Menu" %>
|
|
39
37
|
<% end %>
|