panda-cms 0.8.2 → 0.10.0

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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +83 -4
  3. data/app/components/panda/cms/code_component.rb +117 -39
  4. data/app/components/panda/cms/grid_component.rb +26 -6
  5. data/app/components/panda/cms/menu_component.rb +66 -34
  6. data/app/components/panda/cms/page_menu_component.rb +94 -13
  7. data/app/components/panda/cms/rich_text_component.rb +198 -140
  8. data/app/components/panda/cms/text_component.rb +77 -44
  9. data/app/controllers/panda/cms/admin/base_controller.rb +19 -3
  10. data/app/controllers/panda/cms/admin/dashboard_controller.rb +3 -3
  11. data/app/controllers/panda/cms/admin/files_controller.rb +7 -0
  12. data/app/controllers/panda/cms/admin/menus_controller.rb +47 -3
  13. data/app/controllers/panda/cms/admin/pages_controller.rb +6 -1
  14. data/app/controllers/panda/cms/pages_controller.rb +2 -2
  15. data/app/helpers/panda/cms/application_helper.rb +15 -1
  16. data/app/helpers/panda/cms/asset_helper.rb +14 -3
  17. data/app/javascript/panda/cms/application_panda_cms.js +1 -1
  18. data/app/javascript/panda/cms/controllers/code_editor_controller.js +95 -0
  19. data/app/javascript/panda/cms/controllers/file_gallery_controller.js +128 -0
  20. data/app/javascript/panda/cms/controllers/index.js +48 -13
  21. data/app/javascript/panda/cms/controllers/inline_code_editor_controller.js +96 -0
  22. data/app/javascript/panda/cms/controllers/menu_form_controller.js +40 -0
  23. data/app/javascript/panda/cms/controllers/nested_form_controller.js +35 -0
  24. data/app/javascript/panda/cms/controllers/tree_controller.js +214 -0
  25. data/app/javascript/panda/cms/stimulus-loading.js +5 -7
  26. data/app/models/panda/cms/block_content.rb +9 -0
  27. data/app/models/panda/cms/page.rb +41 -0
  28. data/app/models/panda/cms/post.rb +1 -0
  29. data/app/views/panda/cms/admin/dashboard/show.html.erb +5 -5
  30. data/app/views/panda/cms/admin/files/_file_details.html.erb +45 -0
  31. data/app/views/panda/cms/admin/files/index.html.erb +11 -118
  32. data/app/views/panda/cms/admin/forms/index.html.erb +2 -2
  33. data/app/views/panda/cms/admin/forms/new.html.erb +1 -2
  34. data/app/views/panda/cms/admin/forms/show.html.erb +15 -30
  35. data/app/views/panda/cms/admin/menus/_menu_item_fields.html.erb +11 -0
  36. data/app/views/panda/cms/admin/menus/edit.html.erb +64 -0
  37. data/app/views/panda/cms/admin/menus/index.html.erb +3 -2
  38. data/app/views/panda/cms/admin/menus/new.html.erb +40 -0
  39. data/app/views/panda/cms/admin/pages/edit.html.erb +15 -9
  40. data/app/views/panda/cms/admin/pages/index.html.erb +49 -11
  41. data/app/views/panda/cms/admin/pages/new.html.erb +3 -11
  42. data/app/views/panda/cms/admin/posts/_form.html.erb +4 -14
  43. data/app/views/panda/cms/admin/posts/edit.html.erb +2 -2
  44. data/app/views/panda/cms/admin/posts/index.html.erb +3 -3
  45. data/app/views/panda/cms/admin/posts/new.html.erb +1 -1
  46. data/app/views/panda/cms/admin/settings/bulk_editor/new.html.erb +1 -1
  47. data/app/views/panda/cms/admin/settings/index.html.erb +3 -3
  48. data/config/importmap.rb +4 -6
  49. data/config/initializers/panda/cms/healthcheck_log_silencer.rb.disabled +31 -0
  50. data/config/initializers/panda/cms.rb +52 -10
  51. data/config/routes.rb +4 -2
  52. data/db/migrate/20240305000000_convert_html_content_to_editor_js.rb +2 -2
  53. data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +6 -1
  54. data/db/migrate/20250809231125_migrate_users_to_panda_core.rb +23 -21
  55. data/db/migrate/20251104150640_add_cached_last_updated_at_to_panda_cms_pages.rb +22 -0
  56. data/db/migrate/20251104172242_add_page_type_to_panda_cms_pages.rb +6 -0
  57. data/db/migrate/20251104172638_set_page_types_for_existing_pages.rb +27 -0
  58. data/db/migrate/20251105000001_add_pending_review_status_to_pages_and_posts.panda_cms.rb +21 -0
  59. data/lib/generators/panda/cms/install_generator.rb +2 -5
  60. data/lib/panda/cms/asset_loader.rb +36 -16
  61. data/lib/panda/cms/debug.rb +29 -0
  62. data/lib/panda/cms/engine.rb +107 -48
  63. data/lib/panda/cms/features.rb +52 -0
  64. data/lib/panda-cms/version.rb +1 -1
  65. data/lib/panda-cms.rb +5 -6
  66. data/lib/tasks/assets.rake +5 -52
  67. data/lib/tasks/panda_cms_tasks.rake +16 -0
  68. metadata +22 -29
  69. data/app/components/panda/cms/admin/container_component.html.erb +0 -13
  70. data/app/components/panda/cms/admin/flash_message_component.html.erb +0 -31
  71. data/app/components/panda/cms/admin/panel_component.html.erb +0 -7
  72. data/app/components/panda/cms/admin/slideover_component.html.erb +0 -9
  73. data/app/components/panda/cms/admin/slideover_component.rb +0 -15
  74. data/app/components/panda/cms/admin/statistics_component.html.erb +0 -4
  75. data/app/components/panda/cms/admin/statistics_component.rb +0 -16
  76. data/app/components/panda/cms/admin/tab_bar_component.html.erb +0 -35
  77. data/app/components/panda/cms/admin/tab_bar_component.rb +0 -15
  78. data/app/components/panda/cms/admin/table_component.html.erb +0 -29
  79. data/app/components/panda/cms/admin/user_activity_component.html.erb +0 -7
  80. data/app/components/panda/cms/admin/user_activity_component.rb +0 -20
  81. data/app/components/panda/cms/admin/user_display_component.html.erb +0 -17
  82. data/app/components/panda/cms/admin/user_display_component.rb +0 -21
  83. data/app/components/panda/cms/grid_component.html.erb +0 -6
  84. data/app/components/panda/cms/menu_component.html.erb +0 -6
  85. data/app/components/panda/cms/page_menu_component.html.erb +0 -21
  86. data/app/components/panda/cms/rich_text_component.html.erb +0 -90
  87. data/app/views/layouts/panda/cms/application.html.erb +0 -42
  88. data/app/views/panda/cms/admin/shared/_breadcrumbs.html.erb +0 -28
  89. data/app/views/panda/cms/admin/shared/_flash.html.erb +0 -5
  90. data/app/views/panda/cms/admin/shared/_sidebar.html.erb +0 -41
  91. data/app/views/panda/cms/shared/_footer.html.erb +0 -2
  92. data/app/views/panda/cms/shared/_header.html.erb +0 -25
  93. data/config/initializers/panda/cms/healthcheck_log_silencer.rb +0 -13
@@ -0,0 +1,214 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["row", "toggle", "container"]
5
+ static values = {
6
+ collapsed: { type: Array, default: [] }
7
+ }
8
+
9
+ connect() {
10
+ const hasStoredState = localStorage.getItem('panda-cms-pages-collapsed')
11
+
12
+ if (hasStoredState) {
13
+ this.loadCollapsedState()
14
+ } else {
15
+ this.initializeTree()
16
+ }
17
+ }
18
+
19
+ // Helper to get the actual table row element from our target div
20
+ getTableRow(rowTarget) {
21
+ return rowTarget.closest('.table-row')
22
+ }
23
+
24
+ initializeTree() {
25
+ // Collapse all level 1 pages (direct children of Home) by default
26
+ this.rowTargets.forEach(row => {
27
+ const level = parseInt(row.dataset.level)
28
+ const pageId = row.dataset.pageId
29
+
30
+ // If it's a level 1 page with children, mark it as collapsed
31
+ if (level === 1) {
32
+ const hasChildren = row.querySelector('[data-tree-target="toggle"]')
33
+ if (hasChildren) {
34
+ this.collapsedValue = [...this.collapsedValue, pageId]
35
+ this.updateToggleIcon(pageId, true)
36
+ }
37
+ }
38
+
39
+ // Hide everything below level 1 (level > 1)
40
+ if (level > 1) {
41
+ const tableRow = this.getTableRow(row)
42
+ if (tableRow) tableRow.style.display = 'none'
43
+ }
44
+ })
45
+
46
+ // Save the initial collapsed state
47
+ this.saveCollapsedState()
48
+
49
+ // Fade in the tree after initialization
50
+ this.showTree()
51
+ }
52
+
53
+ toggle(event) {
54
+ event.preventDefault()
55
+ const row = event.currentTarget.closest('[data-tree-target="row"]')
56
+ const pageId = row.dataset.pageId
57
+ const level = parseInt(row.dataset.level)
58
+
59
+ if (this.isCollapsed(pageId)) {
60
+ this.expand(pageId, level)
61
+ } else {
62
+ this.collapse(pageId, level)
63
+ }
64
+
65
+ this.saveCollapsedState()
66
+ }
67
+
68
+ collapse(pageId, level) {
69
+ // Add to collapsed set
70
+ if (!this.collapsedValue.includes(pageId)) {
71
+ this.collapsedValue = [...this.collapsedValue, pageId]
72
+ }
73
+
74
+ // Hide all descendant rows
75
+ const descendants = this.getDescendantRows(pageId, level)
76
+ descendants.forEach(row => {
77
+ const tableRow = this.getTableRow(row)
78
+ if (tableRow) {
79
+ tableRow.style.display = 'none'
80
+ }
81
+ })
82
+
83
+ // Update toggle icon
84
+ this.updateToggleIcon(pageId, true)
85
+ }
86
+
87
+ expand(pageId, level) {
88
+ // Remove from collapsed set
89
+ this.collapsedValue = this.collapsedValue.filter(id => id !== pageId)
90
+
91
+ // Show direct children only (they will handle their own children)
92
+ const directChildren = this.getDirectChildRows(pageId, level)
93
+ directChildren.forEach(row => {
94
+ const tableRow = this.getTableRow(row)
95
+ if (tableRow) tableRow.style.display = ''
96
+ })
97
+
98
+ // Update toggle icon
99
+ this.updateToggleIcon(pageId, false)
100
+ }
101
+
102
+ getDescendantRows(pageId, parentLevel) {
103
+ const allRows = this.rowTargets
104
+ const parentIndex = allRows.findIndex(row => row.dataset.pageId === pageId)
105
+ const descendants = []
106
+
107
+ for (let i = parentIndex + 1; i < allRows.length; i++) {
108
+ const rowLevel = parseInt(allRows[i].dataset.level)
109
+ if (rowLevel <= parentLevel) break
110
+ descendants.push(allRows[i])
111
+ }
112
+
113
+ return descendants
114
+ }
115
+
116
+ getDirectChildRows(pageId, parentLevel) {
117
+ const allRows = this.rowTargets
118
+ const parentIndex = allRows.findIndex(row => row.dataset.pageId === pageId)
119
+ const children = []
120
+
121
+ for (let i = parentIndex + 1; i < allRows.length; i++) {
122
+ const rowLevel = parseInt(allRows[i].dataset.level)
123
+ if (rowLevel <= parentLevel) break
124
+ if (rowLevel === parentLevel + 1) children.push(allRows[i])
125
+ }
126
+
127
+ return children
128
+ }
129
+
130
+ isCollapsed(pageId) {
131
+ return this.collapsedValue.includes(pageId)
132
+ }
133
+
134
+ updateToggleIcon(pageId, collapsed) {
135
+ const row = this.rowTargets.find(r => r.dataset.pageId === pageId)
136
+ if (!row) return
137
+
138
+ const toggle = row.querySelector('[data-tree-target="toggle"]')
139
+ if (!toggle) return
140
+
141
+ const icon = toggle.querySelector('i')
142
+ if (icon) {
143
+ if (collapsed) {
144
+ icon.classList.remove('fa-chevron-down')
145
+ icon.classList.add('fa-chevron-right')
146
+ } else {
147
+ icon.classList.remove('fa-chevron-right')
148
+ icon.classList.add('fa-chevron-down')
149
+ }
150
+ }
151
+ }
152
+
153
+ loadCollapsedState() {
154
+ try {
155
+ const stored = localStorage.getItem('panda-cms-pages-collapsed')
156
+ if (stored) {
157
+ this.collapsedValue = JSON.parse(stored)
158
+
159
+ // First, show all pages at level 1 (direct children of Home)
160
+ this.rowTargets.forEach(row => {
161
+ const level = parseInt(row.dataset.level)
162
+ if (level === 1) {
163
+ const tableRow = this.getTableRow(row)
164
+ if (tableRow) tableRow.style.display = ''
165
+ }
166
+ })
167
+
168
+ // Then apply collapsed state - hiding descendants of collapsed items
169
+ this.collapsedValue.forEach(pageId => {
170
+ const row = this.rowTargets.find(r => r.dataset.pageId === pageId)
171
+ if (row) {
172
+ const level = parseInt(row.dataset.level)
173
+ this.collapse(pageId, level)
174
+ }
175
+ })
176
+
177
+ // For level 1 items NOT in collapsed list, show their children
178
+ this.rowTargets.forEach(row => {
179
+ const level = parseInt(row.dataset.level)
180
+ const pageId = row.dataset.pageId
181
+ const hasToggle = row.querySelector('[data-tree-target="toggle"]')
182
+
183
+ if (level === 1 && hasToggle && !this.isCollapsed(pageId)) {
184
+ // This level 1 item is expanded, show its direct children
185
+ this.getDirectChildRows(pageId, level).forEach(childRow => {
186
+ const tableRow = this.getTableRow(childRow)
187
+ if (tableRow) tableRow.style.display = ''
188
+ })
189
+ }
190
+ })
191
+
192
+ // Fade in the tree after loading state
193
+ this.showTree()
194
+ }
195
+ } catch (e) {
196
+ console.error('Error loading collapsed state:', e)
197
+ }
198
+ }
199
+
200
+ showTree() {
201
+ // Fade in the container
202
+ if (this.hasContainerTarget) {
203
+ this.containerTarget.style.opacity = '1'
204
+ }
205
+ }
206
+
207
+ saveCollapsedState() {
208
+ try {
209
+ localStorage.setItem('panda-cms-pages-collapsed', JSON.stringify(this.collapsedValue))
210
+ } catch (e) {
211
+ console.error('Error saving collapsed state:', e)
212
+ }
213
+ }
214
+ }
@@ -1,14 +1,12 @@
1
1
  // Stimulus loading utilities for Panda CMS
2
2
  // This provides the loading functionality that would normally come from stimulus-rails
3
3
 
4
- import { Application } from "@hotwired/stimulus"
4
+ // Import the shared Stimulus application from Panda Core
5
+ // This ensures all controllers (Core and CMS) are registered in the same application
6
+ import { application } from "/panda/core/application.js"
5
7
 
6
- const application = Application.start()
7
-
8
- // Configure debug mode based on environment
9
- const railsEnv = document.body?.dataset?.environment || "production";
10
- application.debug = railsEnv === "development"
11
- window.Stimulus = application
8
+ // The application is already started and configured in Core
9
+ // No need to start it again or configure debug mode
12
10
 
13
11
  // Auto-registration functionality
14
12
  function eagerLoadControllersFrom(context) {
@@ -12,8 +12,17 @@ module Panda
12
12
 
13
13
  validates :block, presence: true, uniqueness: {scope: :page}
14
14
 
15
+ after_save :refresh_page_cached_timestamp
16
+ after_destroy :refresh_page_cached_timestamp
17
+
15
18
  store_accessor :content, [], prefix: true
16
19
  store_accessor :cached_content, [], prefix: true
20
+
21
+ private
22
+
23
+ def refresh_page_cached_timestamp
24
+ page&.refresh_last_updated_at!
25
+ end
17
26
  end
18
27
  end
19
28
  end
@@ -38,12 +38,22 @@ module Panda
38
38
  enum :status, {
39
39
  active: "active",
40
40
  draft: "draft",
41
+ pending_review: "pending_review",
41
42
  hidden: "hidden",
42
43
  archived: "archived"
43
44
  }
44
45
 
46
+ enum :page_type, {
47
+ standard: "standard",
48
+ hidden_type: "hidden",
49
+ system: "system",
50
+ posts: "posts",
51
+ code: "code"
52
+ }, prefix: :type
53
+
45
54
  # Callbacks
46
55
  after_save :handle_after_save
56
+ before_save :update_cached_last_updated_at
47
57
 
48
58
  #
49
59
  # Update any menus which include this page or its parent as a menu item
@@ -56,6 +66,31 @@ module Panda
56
66
  menus_of_parent.find_each(&:generate_auto_menu_items)
57
67
  end
58
68
 
69
+ #
70
+ # Returns the most recent update time between the page and its block contents
71
+ # Uses cached value for performance
72
+ #
73
+ # @return [Time] The most recent updated_at timestamp
74
+ # @visibility public
75
+ #
76
+ def last_updated_at
77
+ cached_last_updated_at || updated_at
78
+ end
79
+
80
+ #
81
+ # Refresh the cached last updated timestamp
82
+ # Used when block contents are updated
83
+ #
84
+ # @return [Time] The updated timestamp
85
+ # @visibility public
86
+ #
87
+ def refresh_last_updated_at!
88
+ block_content_updated_at = block_contents.maximum(:updated_at)
89
+ new_timestamp = [updated_at, block_content_updated_at].compact.max
90
+ update_column(:cached_last_updated_at, new_timestamp)
91
+ new_timestamp
92
+ end
93
+
59
94
  private
60
95
 
61
96
  def validate_unique_path_in_scope
@@ -124,6 +159,12 @@ module Panda
124
159
  destination_path: new_path
125
160
  )
126
161
  end
162
+
163
+ def update_cached_last_updated_at
164
+ # Will be set to updated_at automatically during save
165
+ # Block content updates will call refresh_last_updated_at! separately
166
+ self.cached_last_updated_at = Time.current
167
+ end
127
168
  end
128
169
  end
129
170
  end
@@ -33,6 +33,7 @@ module Panda
33
33
  enum :status, {
34
34
  active: "active",
35
35
  draft: "draft",
36
+ pending_review: "pending_review",
36
37
  hidden: "hidden",
37
38
  archived: "archived"
38
39
  }
@@ -1,12 +1,12 @@
1
1
  <div class="" data-controller="dashboard">
2
2
  <%= render Panda::Core::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_cms_page_path) %>
3
+ <% container.heading(text: "Dashboard", level: 1) do |heading| %>
4
+ <% heading.button(action: :add, text: "Add Page", link: 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::CMS::Admin::StatisticsComponent.new(metric: "Views Today", value: Panda::CMS::Visit.group_by_day(:visited_at, last: 1).count.values.first) %>
8
- <%= render Panda::CMS::Admin::StatisticsComponent.new(metric: "Views Last Week", value: Panda::CMS::Visit.group_by_week(:visited_at, last: 1).count.values.first) %>
9
- <%= render Panda::CMS::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) %>
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) %>
10
10
  </dl>
11
11
  <% end %>
12
12
  </div>
@@ -0,0 +1,45 @@
1
+ <div>
2
+ <div class="block overflow-hidden w-full rounded-lg aspect-h-7 aspect-w-10">
3
+ <% if file.image? %>
4
+ <%= image_tag main_app.rails_blob_path(file, only_path: true), alt: file.filename.to_s, class: "object-cover" %>
5
+ <% else %>
6
+ <div class="flex items-center justify-center h-full bg-gray-100">
7
+ <div class="text-center">
8
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
9
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
10
+ </svg>
11
+ <p class="mt-1 text-xs text-gray-500 uppercase"><%= file.content_type&.split("/")&.last || "file" %></p>
12
+ </div>
13
+ </div>
14
+ <% end %>
15
+ </div>
16
+ <div class="flex justify-between items-start mt-4">
17
+ <div>
18
+ <h2 class="text-lg font-medium text-gray-900"><span class="sr-only">Details for </span><%= file.filename %></h2>
19
+ <p class="text-sm font-medium text-gray-500"><%= number_to_human_size(file.byte_size) %></p>
20
+ </div>
21
+ </div>
22
+ </div>
23
+
24
+ <div>
25
+ <h3 class="font-medium text-gray-900">Information</h3>
26
+ <dl class="mt-2 border-t border-b border-gray-200 divide-y divide-gray-200">
27
+ <div class="flex justify-between py-3 text-sm font-medium">
28
+ <dt class="text-gray-500">Created</dt>
29
+ <dd class="text-gray-900 whitespace-nowrap"><%= file.created_at.strftime("%B %-d, %Y") %></dd>
30
+ </div>
31
+ <div class="flex justify-between py-3 text-sm font-medium">
32
+ <dt class="text-gray-500">Content Type</dt>
33
+ <dd class="text-gray-900 whitespace-nowrap"><%= file.content_type %></dd>
34
+ </div>
35
+ <div class="flex justify-between py-3 text-sm font-medium">
36
+ <dt class="text-gray-500">Checksum</dt>
37
+ <dd class="text-gray-900 whitespace-nowrap font-mono text-xs"><%= file.checksum %></dd>
38
+ </div>
39
+ </dl>
40
+ </div>
41
+
42
+ <div class="flex gap-x-3">
43
+ <%= link_to "Download", main_app.rails_blob_path(file, disposition: "attachment"), class: "flex-1 py-2 px-3 text-sm font-semibold text-white bg-black rounded-md shadow-sm hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-panda-dark text-center" %>
44
+ <%= button_tag "Delete", type: "button", class: "flex-1 py-2 px-3 text-sm font-semibold text-gray-900 bg-white rounded-md ring-1 ring-inset shadow-sm hover:bg-gray-50 ring-mid", data: { confirm: "Are you sure you want to delete this file?" } %>
45
+ </div>
@@ -1,124 +1,17 @@
1
1
  <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
- <% component.with_heading(text: "Files", level: 1) %>
2
+ <% component.heading(text: "Files", level: 1) %>
3
3
 
4
- <% component.with_slideover(title: "File Details") do %>
5
- <div class="pt-4 pb-16 space-y-6">
6
- <div>
7
- <div class="block overflow-hidden w-full rounded-lg aspect-h-7 aspect-w-10">
8
- <img src="https://images.unsplash.com/photo-1582053433976-25c00369fc93?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=512&q=80" alt="" class="object-cover">
9
- </div>
10
- <div class="flex justify-between items-start mt-4">
11
- <div>
12
- <h2 class="text-lg font-medium text-gray-900"><span class="sr-only">Details for </span>IMG_4985.HEIC</h2>
13
- <p class="text-sm font-medium text-gray-500">3.9 MB</p>
14
- </div>
15
- <button type="button" class="flex relative justify-center items-center ml-4 w-8 h-8 text-gray-400 bg-white rounded-full hover:text-gray-500 hover:bg-gray-100 focus:ring-2 focus:outline-none focus:ring-panda-dark">
16
- <span class="absolute -inset-1.5"></span>
17
- <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
18
- <path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
19
- </svg>
20
- <span class="sr-only">Favorite</span>
21
- </button>
22
- </div>
23
- </div>
24
- <div>
25
- <h3 class="font-medium text-gray-900">Information</h3>
26
- <dl class="mt-2 border-t border-b border-gray-200 divide-y divide-gray-200">
27
- <div class="flex justify-between py-3 text-sm font-medium">
28
- <dt class="text-gray-500">Uploaded by</dt>
29
- <dd class="text-gray-900 whitespace-nowrap">Marie Culver</dd>
30
- </div>
31
- <div class="flex justify-between py-3 text-sm font-medium">
32
- <dt class="text-gray-500">Created</dt>
33
- <dd class="text-gray-900 whitespace-nowrap">June 8, 2020</dd>
34
- </div>
35
- <div class="flex justify-between py-3 text-sm font-medium">
36
- <dt class="text-gray-500">Last modified</dt>
37
- <dd class="text-gray-900 whitespace-nowrap">June 8, 2020</dd>
38
- </div>
39
- <div class="flex justify-between py-3 text-sm font-medium">
40
- <dt class="text-gray-500">Dimensions</dt>
41
- <dd class="text-gray-900 whitespace-nowrap">4032 x 3024</dd>
42
- </div>
43
- <div class="flex justify-between py-3 text-sm font-medium">
44
- <dt class="text-gray-500">Resolution</dt>
45
- <dd class="text-gray-900 whitespace-nowrap">72 x 72</dd>
46
- </div>
47
- </dl>
48
- </div>
49
- <div>
50
- <h3 class="font-medium text-gray-900">Description</h3>
51
- <div class="flex justify-between items-center mt-2">
52
- <p class="text-sm italic text-gray-500">Add a description to this image.</p>
53
- <button type="button" class="flex relative justify-center items-center w-8 h-8 text-gray-400 bg-white rounded-full hover:text-gray-500 hover:bg-gray-100 focus:ring-2 focus:outline-none focus:ring-panda-dark">
54
- <span class="absolute -inset-1.5"></span>
55
- <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
56
- <path d="M2.695 14.763l-1.262 3.154a.5.5 0 00.65.65l3.155-1.262a4 4 0 001.343-.885L17.5 5.5a2.121 2.121 0 00-3-3L3.58 13.42a4 4 0 00-.885 1.343z" />
57
- </svg>
58
- <span class="sr-only">Add description</span>
59
- </button>
60
- </div>
61
- </div>
62
- <div>
63
- <h3 class="font-medium text-gray-900">Shared with</h3>
64
- <ul role="list" class="mt-2 border-t border-b border-gray-200 divide-y divide-gray-200">
65
- <li class="flex justify-between items-center py-3">
66
- <div class="flex items-center">
67
- <img src="https://images.unsplash.com/photo-1502685104226-ee32379fefbe?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=3&w=1024&h=1024&q=80" alt="" class="w-8 h-8 rounded-full">
68
- <p class="ml-4 text-sm font-medium text-gray-900">Aimee Douglas</p>
69
- </div>
70
- <button type="button" class="ml-6 text-sm font-medium bg-white rounded-md focus:ring-2 focus:ring-offset-2 focus:outline-none text-panda-dark hover:text-panda-dark focus:ring-panda-dark">Remove<span class="sr-only"> Aimee Douglas</span></button>
71
- </li>
72
- <li class="flex justify-between items-center py-3">
73
- <div class="flex items-center">
74
- <img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixqx=oilqXxSqey&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="" class="w-8 h-8 rounded-full">
75
- <p class="ml-4 text-sm font-medium text-gray-900">Andrea McMillan</p>
76
- </div>
77
- <button type="button" class="ml-6 text-sm font-medium bg-white rounded-md focus:ring-2 focus:ring-offset-2 focus:outline-none text-panda-dark hover:text-panda-dark focus:ring-panda-dark">Remove<span class="sr-only"> Andrea McMillan</span></button>
78
- </li>
79
-
80
- <li class="flex justify-between items-center py-2">
81
- <button type="button" class="flex items-center p-1 -ml-1 bg-white rounded-md focus:ring-2 focus:outline-none group focus:ring-panda-dark">
82
- <span class="flex justify-center items-center w-8 h-8 text-gray-400 rounded-full border-2 border-gray-300 border-dashed">
83
- <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
84
- <path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
85
- </svg>
86
- </span>
87
- <span class="ml-4 text-sm font-medium text-panda-dark group-hover:text-panda-dark">Share</span>
88
- </button>
89
- </li>
90
- </ul>
91
- </div>
92
- <div class="flex gap-x-3">
93
- <button type="button" class="flex-1 py-2 px-3 text-sm font-semibold text-white bg-black rounded-md shadow-sm hover:bg-black focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-panda-dark">Download</button>
94
- <button type="button" class="flex-1 py-2 px-3 text-sm font-semibold text-gray-900 bg-white rounded-md ring-1 ring-inset shadow-sm hover:bg-gray-50 ring-mid">Delete</button>
95
- </div>
4
+ <% component.slideover(title: "File Details") do %>
5
+ <div class="pt-4 pb-16 space-y-6" data-file-gallery-target="slideoverContent">
6
+ <% if @selected_file %>
7
+ <%= render partial: "file_details", locals: { file: @selected_file } %>
8
+ <% else %>
9
+ <p class="text-sm text-gray-500">Select a file to view details</p>
10
+ <% end %>
96
11
  </div>
97
12
  <% end %>
98
13
 
99
- <% component.with_tab_bar do %>
100
- <%= component.with_tab(name: "Recently Viewed", active: true) %>
101
- <%= component.with_tab(name: "Recently Added") %>
102
- <%= component.with_tab(name: "Favourited") %>
103
- <% end %>
104
-
105
- <section>
106
- <h2 id="gallery-heading" class="sr-only">Recently viewed</h2>
107
- <ul role="list" class="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 md:grid-cols-4 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8">
108
- <li class="relative">
109
- <!-- Current: "ring-2 ring-panda-dark ring-offset-2", Default: "focus-within:ring-2 focus-within:ring-panda-dark focus-within:ring-offset-2 focus-within:ring-offset-gray-100" -->
110
- <div class="block overflow-hidden w-full bg-gray-100 rounded-lg ring-2 ring-offset-2 ring-panda-dark aspect-w-10 aspect-h-7 group">
111
- <!-- Current: "", Default: "group-hover:opacity-75" -->
112
- <img src="https://images.unsplash.com/photo-1582053433976-25c00369fc93?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=512&q=80" alt="" class="object-cover pointer-events-none">
113
- <button type="button" class="absolute inset-0 focus:outline-none">
114
- <span class="sr-only">View details for IMG_4985.HEIC</span>
115
- </button>
116
- </div>
117
- <p class="block mt-2 text-sm font-medium text-gray-900 pointer-events-none truncate">IMG_4985.HEIC</p>
118
- <p class="block text-sm font-medium text-gray-500 pointer-events-none">3.9 MB</p>
119
- </li>
120
-
121
- <!-- More files... -->
122
- </ul>
123
- </section>
14
+ <div data-controller="file-gallery" class="pb-24">
15
+ <%= render Panda::Core::Admin::FileGalleryComponent.new(files: @files, selected_file: @selected_file) %>
16
+ </div>
124
17
  <% end %>
@@ -1,8 +1,8 @@
1
1
  <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
- <% component.with_heading(text: "Forms", level: 1) do |heading| %>
2
+ <% component.heading(text: "Forms", level: 1, icon: "fa-solid fa-inbox") do |heading| %>
3
3
  <% end %>
4
4
 
5
- <%= render Panda::Core::Admin::TableComponent.new(term: "form", rows: forms) do |table| %>
5
+ <%= render Panda::Core::Admin::TableComponent.new(term: "form", rows: forms, icon: "fa-solid fa-inbox") do |table| %>
6
6
  <% table.column("Name") { |form| block_link_to form.name, admin_cms_form_path(form) } %>
7
7
  <% table.column("Status") { |form| render Panda::Core::Admin::TagComponent.new(status: :active) } %>
8
8
  <% table.column("Last Submission") do |form| %>
@@ -1,6 +1,5 @@
1
1
  <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
- <% component.with_heading(text: "Add Page", level: 1) do |heading| %>
3
- <% end %>
2
+ <% component.heading(text: "Add Form", level: 1) %>
4
3
  <%= panda_cms_form_with model: page, url: admin_cms_pages_path, method: :post do |f| %>
5
4
  <% options = nested_set_options(Panda::CMS::Page, page) { |i| "#{"-" * i.level} #{i.title} (#{i.path})" } %>
6
5
  <div data-controller="slug">
@@ -1,35 +1,20 @@
1
1
  <%= render Panda::Core::Admin::ContainerComponent.new do |component| %>
2
- <% component.with_heading(text: form.name, level: 1) do |heading| %>
2
+ <% component.heading(text: form.name, level: 1) do |heading| %>
3
3
  <% end %>
4
4
 
5
- <div class="overflow-x-auto -mx-4 rounded-lg ring-1 sm:mx-0 sm:rounded-lg ring-mid">
6
- <table class="min-w-full divide-y divide-gray-300 border-collapse table-auto">
7
- <thead>
8
- <tr>
9
- <% fields.each do |field, title| %>
10
- <th scope="col" class="py-3.5 px-3 text-sm font-semibold text-left text-gray-900"><%= title %></th>
11
- <% end %>
12
- <th scope="col" class="py-3.5 px-3 text-sm font-semibold text-left text-gray-900">Submitted</th>
13
- </tr>
14
- </thead>
15
- <tbody class="bg-white divide-y divide-gray-200">
16
- <% submissions.each do |submission| %>
17
- <tr class="relative bg-white cursor-pointer hover:bg-gray-50">
18
- <% fields.each do |field| %>
19
- <td class="py-5 px-3 text-sm text-gray-500">
20
- <% if field[0] == "email" || field[0] == "email_address" %>
21
- <a href="mailto:<%= submission.data[field[0]] %>" class="border-b border-gray-500 hover:text-gray-900"><%= submission.data[field[0]] %></a>
22
- <% else %>
23
- <%= simple_format(submission.data[field[0]]) %>
24
- <% end %>
25
- </td>
26
- <% end %>
27
- <td class="py-5 px-3 text-sm text-gray-500 whitespace-nowrap">
28
- <div class="text-gray-500"><%= time_ago_in_words(submission.created_at) %> ago</div>
29
- </td>
30
- </tr>
5
+ <%= render Panda::Core::Admin::TableComponent.new(term: "submission", rows: submissions) do |table| %>
6
+ <% fields.each do |field, title| %>
7
+ <% table.column(title) do |submission| %>
8
+ <% if field[0] == "email" || field[0] == "email_address" %>
9
+ <a href="mailto:<%= submission.data[field[0]] %>" class="border-b border-gray-500 hover:text-gray-900"><%= submission.data[field[0]] %></a>
10
+ <% else %>
11
+ <%= simple_format(submission.data[field[0]]) %>
31
12
  <% end %>
32
- </tbody>
33
- </table>
34
- </div>
13
+ <% end %>
14
+ <% end %>
15
+
16
+ <% table.column("Submitted") do |submission| %>
17
+ <%= time_ago_in_words(submission.created_at) %> ago
18
+ <% end %>
19
+ <% end %>
35
20
  <% end %>
@@ -0,0 +1,11 @@
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, placeholder: "Menu item 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, placeholder: "External URL (optional)" %>
5
+
6
+ <div class="mt-3">
7
+ <%= render Panda::Core::Admin::ButtonComponent.new(text: "Remove", action: :delete, link: "#", size: :small, data: { action: "click->nested-form#remove" }) %>
8
+ </div>
9
+
10
+ <%= form.hidden_field :_destroy %>
11
+ </div>