panda-cms 0.10.2 → 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 +77 -0
- 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 +2 -2
- data/app/components/panda/cms/menu_component.rb +9 -2
- data/app/components/panda/cms/rich_text_component.rb +1 -1
- data/app/components/panda/cms/text_component.rb +1 -1
- data/app/javascript/panda/cms/controllers/editor_form_controller.js +3 -3
- data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +4 -4
- data/app/jobs/panda/cms/record_visit_job.rb +2 -1
- data/app/models/panda/cms/visit.rb +16 -1
- data/app/services/panda/social/instagram_feed_service.rb +54 -54
- data/app/views/panda/cms/admin/dashboard/show.html.erb +10 -3
- data/app/views/panda/cms/admin/forms/new.html.erb +1 -1
- data/app/views/panda/cms/admin/menus/edit.html.erb +1 -1
- data/app/views/panda/cms/admin/menus/new.html.erb +1 -1
- data/app/views/panda/cms/admin/pages/edit.html.erb +77 -3
- data/app/views/panda/cms/admin/posts/_form.html.erb +6 -6
- data/app/views/panda/cms/shared/_favicons.html.erb +7 -7
- data/config/importmap.rb +0 -1
- data/config/initializers/groupdate.rb +5 -0
- data/config/locales/en.yml +1 -2
- 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/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 +18 -4
- data/lib/panda/cms/engine/autoload_config.rb +18 -0
- data/lib/panda/cms/engine/route_config.rb +1 -2
- data/lib/panda/cms/engine.rb +4 -23
- data/lib/{panda-cms → panda/cms}/version.rb +1 -1
- data/lib/panda/cms.rb +3 -1
- data/lib/panda-cms.rb +8 -1
- data/lib/tasks/ci.rake +0 -0
- metadata +13 -46
- 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/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/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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 680bef797106fd65c694455aac3c55bc7799bfdfba990997a96bec44f325f780
|
|
4
|
+
data.tar.gz: a8485d6a4da3f5623531eb14ae6188226204ab8b2bce3381baa3f1c6e3b7caf4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3cba49016e04bc8ec5a2f1011897687eeb2478c451c0a9d38feef27605e4ddfe674c9d8543ee92e33b76049d3ecb08bac0bab1e6b790576c5f043683dbca606e
|
|
7
|
+
data.tar.gz: bb6c39903cdf5412c99dae06721a3d908dc0ed6d2e2c6a5c443eb96c6e6e43965029530a2206bce40bf2a8c06ddfd8fafbdf97d341c6f9739269f64693240479
|
data/README.md
CHANGED
|
@@ -171,6 +171,83 @@ expect(page.title).to eq("Home")
|
|
|
171
171
|
|
|
172
172
|
When testing models with file validations or complex callbacks, use the helper methods in `spec/models/panda/cms/page_spec.rb` as a reference.
|
|
173
173
|
|
|
174
|
+
## 🚀 Running CI Locally
|
|
175
|
+
|
|
176
|
+
This project uses a **deterministic CI environment**, based on a single Docker
|
|
177
|
+
image (`panda-cms-test`). This ensures:
|
|
178
|
+
|
|
179
|
+
- identical Ruby/Node/Chrome versions everywhere
|
|
180
|
+
- no drift between local / Docker / GitHub Actions / act
|
|
181
|
+
- fast, stable, reproducible tests
|
|
182
|
+
|
|
183
|
+
There are **three** supported ways to run the full CI suite locally.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### 1. Run full CI via Docker Compose
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
bin/ci build # build the local test image
|
|
191
|
+
bin/ci local # run full CI stack locally
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
This uses `docker-compose.ci.yml` and reproduces the entire GitHub Actions environment.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
### 2. Run single RSpec execution in the CI container
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
bin/ci test
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
This mounts your project into the container and executes RSpec exactly as CI does.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
### 3. Run GitHub Actions locally using act
|
|
209
|
+
|
|
210
|
+
Install act:
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
brew install act
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Use the project’s `.actrc`:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
-P ubuntu-latest=ghcr.io/tastybamboo/panda-cms-test:local
|
|
220
|
+
--container-options "--shm-size=2gb"
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Then run:
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
bin/ci act
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
This executes **the real GitHub Actions workflow** on your machine.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### 4. Continuous Integration on GitHub
|
|
234
|
+
|
|
235
|
+
GitHub Actions uses the same deterministic container image.
|
|
236
|
+
See:
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
.github/workflows/ci.yml
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### 5. Code Coverage
|
|
245
|
+
|
|
246
|
+
Coverage is produced per-suite (models, requests, libs, system) and merged
|
|
247
|
+
into a unified `coverage/` directory via SimpleCov.
|
|
248
|
+
|
|
249
|
+
Artifacts are uploaded automatically on CI.
|
|
250
|
+
|
|
174
251
|
## License
|
|
175
252
|
|
|
176
253
|
The gem is available as open source under the terms of the [BSD-3-Clause License](https://opensource.org/licenses/bsd-3-clause).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* Panda CMS specific styles go here */
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
module Admin
|
|
6
|
+
class PopularPagesComponent < Panda::Core::Base
|
|
7
|
+
def initialize(popular_pages:, period_name: "All Time")
|
|
8
|
+
@popular_pages = popular_pages
|
|
9
|
+
@period_name = period_name
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def view_template
|
|
13
|
+
render Panda::Core::Admin::PanelComponent.new do |panel|
|
|
14
|
+
panel.heading(text: "Popular Pages (#{@period_name})", level: :panel)
|
|
15
|
+
|
|
16
|
+
panel.body do
|
|
17
|
+
if @popular_pages.any?
|
|
18
|
+
div(class: "overflow-y-auto max-h-96") do
|
|
19
|
+
table(class: "min-w-full divide-y divide-gray-300") do
|
|
20
|
+
thead(class: "sticky top-0 bg-white z-10") do
|
|
21
|
+
tr do
|
|
22
|
+
th(class: "py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0") { "Page" }
|
|
23
|
+
th(class: "px-3 py-3.5 text-left text-sm font-semibold text-gray-900") { "Path" }
|
|
24
|
+
th(class: "px-3 py-3.5 text-right text-sm font-semibold text-gray-900") { "Views" }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
tbody(class: "divide-y divide-gray-200") do
|
|
28
|
+
index = 0
|
|
29
|
+
@popular_pages.each do |page_data|
|
|
30
|
+
tr(class: index.even? ? "bg-indigo-50" : "bg-white") do
|
|
31
|
+
td(class: "whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0") do
|
|
32
|
+
a(
|
|
33
|
+
href: view_context.admin_cms_page_path(page_data.id),
|
|
34
|
+
class: "text-indigo-600 hover:text-indigo-900"
|
|
35
|
+
) { page_data.title }
|
|
36
|
+
end
|
|
37
|
+
td(class: "whitespace-nowrap px-3 py-4 text-sm text-gray-500") do
|
|
38
|
+
a(
|
|
39
|
+
href: page_data.path,
|
|
40
|
+
class: "text-gray-600 hover:text-gray-900",
|
|
41
|
+
target: "_blank"
|
|
42
|
+
) { page_data.path }
|
|
43
|
+
end
|
|
44
|
+
td(class: "whitespace-nowrap px-3 py-4 text-sm text-gray-500 text-right font-semibold") do
|
|
45
|
+
page_data.visit_count.to_s
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
index += 1
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
else
|
|
54
|
+
p(class: "text-sm text-gray-500") { "No page visits recorded yet." }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -17,7 +17,7 @@ module Panda
|
|
|
17
17
|
# Russian doll caching: Cache component output at block_content level
|
|
18
18
|
# Only cache in non-editable mode (public-facing pages)
|
|
19
19
|
if should_cache?
|
|
20
|
-
raw cache_component_output
|
|
20
|
+
raw cache_component_output.to_s.html_safe
|
|
21
21
|
else
|
|
22
22
|
render_content
|
|
23
23
|
end
|
|
@@ -84,7 +84,7 @@ module Panda
|
|
|
84
84
|
data: {inline_code_editor_target: "codeInput"},
|
|
85
85
|
class: "w-full h-64 p-3 font-mono text-sm border border-gray-300 rounded focus:ring-primary focus:border-primary",
|
|
86
86
|
placeholder: "Enter your HTML/embed code here..."
|
|
87
|
-
) {
|
|
87
|
+
) { plain @code_content.to_s }
|
|
88
88
|
|
|
89
89
|
div(class: "mt-3 flex justify-end space-x-2") do
|
|
90
90
|
button(type: "button",
|
|
@@ -6,7 +6,7 @@ module Panda
|
|
|
6
6
|
# @param name [String] The name of the menu to render
|
|
7
7
|
# @param current_path [String] The current request path for highlighting active items
|
|
8
8
|
# @param styles [Hash] CSS classes for menu items (default, active, inactive)
|
|
9
|
-
# @param overrides [Hash] Menu item overrides
|
|
9
|
+
# @param overrides [Hash] Menu item overrides - supports :hidden_items array to hide specific menu items by text
|
|
10
10
|
# @param render_page_menu [Boolean] Whether to render sub-page menus
|
|
11
11
|
# @param page_menu_styles [Hash] Styles for the page menu component
|
|
12
12
|
class MenuComponent < Panda::Core::Base
|
|
@@ -54,7 +54,14 @@ module Panda
|
|
|
54
54
|
items.order(:lft).to_a # Convert to array for caching
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
# Filter menu items based on overrides
|
|
58
|
+
filtered_menu_items = if @overrides[:hidden_items].present?
|
|
59
|
+
menu_items.reject { |item| @overrides[:hidden_items].include?(item.text) }
|
|
60
|
+
else
|
|
61
|
+
menu_items
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
@processed_menu_items = filtered_menu_items.map do |menu_item|
|
|
58
65
|
add_css_classes_to_item(menu_item)
|
|
59
66
|
menu_item
|
|
60
67
|
end
|
|
@@ -277,7 +277,7 @@ module Panda
|
|
|
277
277
|
|
|
278
278
|
def render_content_to_string
|
|
279
279
|
# Render the component HTML to a string for caching
|
|
280
|
-
|
|
280
|
+
view_context.content_tag(:div, @rendered_content.html_safe, class: "panda-cms-content", **element_attrs)
|
|
281
281
|
end
|
|
282
282
|
end
|
|
283
283
|
end
|
|
@@ -129,7 +129,7 @@ module Panda
|
|
|
129
129
|
|
|
130
130
|
def render_content_to_string
|
|
131
131
|
# Phlex doesn't have a direct way to capture output, so we render directly
|
|
132
|
-
|
|
132
|
+
view_context.content_tag(:span, @content.html_safe, **element_attrs)
|
|
133
133
|
end
|
|
134
134
|
end
|
|
135
135
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
|
2
|
-
import { EDITOR_JS_RESOURCES, EDITOR_JS_CSS } from "panda/
|
|
3
|
-
import { ResourceLoader } from "panda/
|
|
2
|
+
import { EDITOR_JS_RESOURCES, EDITOR_JS_CSS } from "panda/editor/editor_js_config";
|
|
3
|
+
import { ResourceLoader } from "panda/editor/resource_loader";
|
|
4
4
|
|
|
5
5
|
export default class extends Controller {
|
|
6
6
|
static targets = ["editorContainer", "hiddenField"];
|
|
@@ -50,7 +50,7 @@ export default class extends Controller {
|
|
|
50
50
|
this.editorContainerTarget.appendChild(holderDiv);
|
|
51
51
|
|
|
52
52
|
const { getEditorConfig } = await import(
|
|
53
|
-
"panda/
|
|
53
|
+
"panda/editor/editor_js_config"
|
|
54
54
|
);
|
|
55
55
|
|
|
56
56
|
// Get initial content before creating config
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
import { PlainTextEditor } from "panda/
|
|
3
|
-
import { EditorJSInitializer } from "panda/
|
|
4
|
-
import { EDITOR_JS_RESOURCES, EDITOR_JS_CSS } from "panda/
|
|
5
|
-
import { ResourceLoader } from "panda/
|
|
2
|
+
import { PlainTextEditor } from "panda/editor/plain_text_editor"
|
|
3
|
+
import { EditorJSInitializer } from "panda/editor/editor_js_initializer"
|
|
4
|
+
import { EDITOR_JS_RESOURCES, EDITOR_JS_CSS } from "panda/editor/editor_js_config"
|
|
5
|
+
import { ResourceLoader } from "panda/editor/resource_loader"
|
|
6
6
|
|
|
7
7
|
export default class extends Controller {
|
|
8
8
|
static values = {
|
|
@@ -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
|
|
@@ -4,9 +4,16 @@
|
|
|
4
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>
|
|
@@ -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,7 +5,7 @@
|
|
|
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), {}, { data
|
|
8
|
+
<%= f.select :kind, options_for_select([["Static", "static"], ["Auto", "auto"]], selected: @menu.kind), {}, { "data-action": "change->menu-form#kindChanged" } %>
|
|
9
9
|
|
|
10
10
|
<div data-menu-form-target="startPageField" class="<%= 'hidden' unless @menu.kind == 'auto' %>">
|
|
11
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" } %>
|
|
@@ -5,7 +5,7 @@
|
|
|
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
10
|
<div data-menu-form-target="startPageField" class="hidden">
|
|
11
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" } %>
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
<%= f.check_box :inherit_seo, {
|
|
59
59
|
label: "Inherit SEO from parent page",
|
|
60
60
|
data: {
|
|
61
|
-
page_form_target: "inheritCheckbox"
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
page_form_target: "inheritCheckbox"
|
|
62
|
+
},
|
|
63
|
+
"data-action": "change->page-form#toggleInherit"
|
|
64
64
|
} %>
|
|
65
65
|
<% end %>
|
|
66
66
|
|
|
@@ -158,4 +158,78 @@
|
|
|
158
158
|
editor_iframe_autosave_value: false,
|
|
159
159
|
editor_iframe_assets_value: panda_cms_injectable_assets
|
|
160
160
|
} %>
|
|
161
|
+
|
|
162
|
+
<%# Vanilla JS character counters - works independently of Stimulus %>
|
|
163
|
+
<script type="text/javascript">
|
|
164
|
+
(function() {
|
|
165
|
+
function initCharacterCounters() {
|
|
166
|
+
var fieldConfigs = [
|
|
167
|
+
{ selector: '[data-page-form-target="seoTitle"]', max: 70, label: 'SEO Title' },
|
|
168
|
+
{ selector: '[data-page-form-target="seoDescription"]', max: 160, label: 'SEO Description' },
|
|
169
|
+
{ selector: '[data-page-form-target="ogTitle"]', max: 60, label: 'Social Media Title' },
|
|
170
|
+
{ selector: '[data-page-form-target="ogDescription"]', max: 200, label: 'Social Media Description' }
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
fieldConfigs.forEach(function(config) {
|
|
174
|
+
var field = document.querySelector(config.selector);
|
|
175
|
+
if (!field) return;
|
|
176
|
+
|
|
177
|
+
createCharacterCounter(field, config.max);
|
|
178
|
+
field.addEventListener('input', function() {
|
|
179
|
+
updateCharacterCounter(field, config.max);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function createCharacterCounter(field, maxLength) {
|
|
185
|
+
var container = field.closest('.panda-core-field-container');
|
|
186
|
+
if (!container) return;
|
|
187
|
+
|
|
188
|
+
var counter = container.querySelector('.character-counter');
|
|
189
|
+
if (!counter) {
|
|
190
|
+
counter = document.createElement('div');
|
|
191
|
+
counter.className = 'character-counter text-xs mt-1 text-gray-500 dark:text-gray-400';
|
|
192
|
+
|
|
193
|
+
var errorMsg = container.querySelector('.text-red-600');
|
|
194
|
+
if (errorMsg) {
|
|
195
|
+
errorMsg.parentNode.insertBefore(counter, errorMsg);
|
|
196
|
+
} else {
|
|
197
|
+
container.appendChild(counter);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
updateCharacterCounter(field, maxLength);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function updateCharacterCounter(field, maxLength) {
|
|
205
|
+
var container = field.closest('.panda-core-field-container');
|
|
206
|
+
if (!container) return;
|
|
207
|
+
|
|
208
|
+
var counter = container.querySelector('.character-counter');
|
|
209
|
+
if (!counter) return;
|
|
210
|
+
|
|
211
|
+
var currentLength = field.value.length;
|
|
212
|
+
var remaining = maxLength - currentLength;
|
|
213
|
+
|
|
214
|
+
counter.textContent = currentLength + ' / ' + maxLength + ' characters';
|
|
215
|
+
|
|
216
|
+
counter.className = 'character-counter text-xs mt-1';
|
|
217
|
+
|
|
218
|
+
if (remaining < 0) {
|
|
219
|
+
counter.className += ' text-red-600 dark:text-red-400 font-semibold';
|
|
220
|
+
counter.textContent += ' (' + Math.abs(remaining) + ' over limit)';
|
|
221
|
+
} else if (remaining < 10) {
|
|
222
|
+
counter.className += ' text-yellow-600 dark:text-yellow-400';
|
|
223
|
+
} else {
|
|
224
|
+
counter.className += ' text-gray-500 dark:text-gray-400';
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (document.readyState === 'loading') {
|
|
229
|
+
document.addEventListener('DOMContentLoaded', initCharacterCounters);
|
|
230
|
+
} else {
|
|
231
|
+
initCharacterCounters();
|
|
232
|
+
}
|
|
233
|
+
})();
|
|
234
|
+
</script>
|
|
161
235
|
<% end %>
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
<div data-controller="slug" data-slug-add-date-prefix-value="true">
|
|
5
5
|
<%= f.text_field :title,
|
|
6
6
|
data: {
|
|
7
|
-
"slug-target": "input_text"
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
"slug-target": "input_text"
|
|
8
|
+
},
|
|
9
|
+
"data-action": "focusout->slug#generatePath" %>
|
|
10
10
|
<%= f.text_field :slug,
|
|
11
11
|
data: {
|
|
12
12
|
"slug-target": "output_text"
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
<%= f.hidden_field :content,
|
|
23
23
|
data: {
|
|
24
24
|
editor_form_target: "hiddenField",
|
|
25
|
-
initial_content: editor_content_for(post, local_assigns[:preserved_content])
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
initial_content: editor_content_for(post, local_assigns[:preserved_content])
|
|
26
|
+
},
|
|
27
|
+
"data-action": "change->editor-form#handleContentChange" %>
|
|
28
28
|
<div id="<%= editor_id %>"
|
|
29
29
|
data-editor-form-target="editorContainer"
|
|
30
30
|
class="max-w-full block bg-white pt-1 mb-4 mt-2 border border-mid rounded-md min-h-[300px]">
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
<link rel="apple-touch-icon" sizes="180x180" href="/panda-
|
|
2
|
-
<link rel="icon" type="image/png" sizes="32x32" href="/panda-
|
|
3
|
-
<link rel="icon" type="image/png" sizes="16x16" href="/panda-
|
|
4
|
-
<link rel="manifest" href="/panda-
|
|
5
|
-
<link rel="mask-icon" href="/panda-
|
|
6
|
-
<link rel="shortcut icon" href="/panda-
|
|
1
|
+
<link rel="apple-touch-icon" sizes="180x180" href="/panda-core-assets/favicons/apple-touch-icon.png">
|
|
2
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/panda-core-assets/favicons/favicon-32x32.png">
|
|
3
|
+
<link rel="icon" type="image/png" sizes="16x16" href="/panda-core-assets/favicons/favicon-16x16.png">
|
|
4
|
+
<link rel="manifest" href="/panda-core-assets/favicons/site.webmanifest">
|
|
5
|
+
<link rel="mask-icon" href="/panda-core-assets/favicons/safari-pinned-tab.svg" color="#5bbad5">
|
|
6
|
+
<link rel="shortcut icon" href="/panda-core-assets/favicons/favicon.ico">
|
|
7
7
|
<meta name="msapplication-TileColor" content="#b91d47">
|
|
8
|
-
<meta name="msapplication-config" content="/panda-
|
|
8
|
+
<meta name="msapplication-config" content="/panda-core-assets/favicons/browserconfig.xml">
|
|
9
9
|
<meta name="theme-color" content="#ffffff">
|
data/config/importmap.rb
CHANGED
|
@@ -12,4 +12,3 @@ pin "@editorjs/editorjs", to: "/panda/cms/editor/editorjs.js" # @2.30.6
|
|
|
12
12
|
# Pin the controllers directory
|
|
13
13
|
pin "panda/cms/controllers/index", to: "/panda/cms/controllers/index.js"
|
|
14
14
|
pin_all_from Panda::CMS::Engine.root.join("app/javascript/panda/cms/controllers"), under: "controllers", to: "/panda/cms/controllers"
|
|
15
|
-
pin_all_from Panda::CMS::Engine.root.join("app/javascript/panda/cms/editor"), under: "editor", to: "/panda/cms/editor"
|
data/config/locales/en.yml
CHANGED
|
@@ -2,18 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
class AddNestedSetsToPandaCMSPages < ActiveRecord::Migration[7.1]
|
|
4
4
|
def self.up
|
|
5
|
-
Panda::CMS::Page.where(parent_id: 0).update_all(parent_id: nil)
|
|
6
5
|
add_column :panda_cms_pages, :lft, :integer
|
|
7
6
|
add_column :panda_cms_pages, :rgt, :integer
|
|
8
|
-
|
|
9
|
-
# This is necessary to update :lft and :rgt columns
|
|
10
|
-
Panda::CMS::Page.reset_column_information
|
|
11
|
-
|
|
12
|
-
# Only rebuild if there are existing pages
|
|
13
|
-
# On fresh installs, there won't be any pages yet
|
|
14
|
-
if Panda::CMS::Page.any?
|
|
15
|
-
Panda::CMS::Page.rebuild!
|
|
16
|
-
end
|
|
17
7
|
end
|
|
18
8
|
|
|
19
9
|
def self.down
|