panda-core 0.2.3 → 0.4.1

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +185 -0
  3. data/app/assets/tailwind/application.css +279 -0
  4. data/app/assets/tailwind/tailwind.config.js +21 -0
  5. data/app/components/panda/core/UI/badge.rb +107 -0
  6. data/app/components/panda/core/UI/button.rb +89 -0
  7. data/app/components/panda/core/UI/card.rb +88 -0
  8. data/app/components/panda/core/admin/button_component.rb +46 -28
  9. data/app/components/panda/core/admin/container_component.rb +52 -4
  10. data/app/components/panda/core/admin/flash_message_component.rb +74 -9
  11. data/app/components/panda/core/admin/form_error_component.rb +48 -0
  12. data/app/components/panda/core/admin/form_input_component.rb +50 -0
  13. data/app/components/panda/core/admin/form_select_component.rb +68 -0
  14. data/app/components/panda/core/admin/heading_component.rb +52 -24
  15. data/app/components/panda/core/admin/panel_component.rb +33 -4
  16. data/app/components/panda/core/admin/slideover_component.rb +8 -4
  17. data/app/components/panda/core/admin/statistics_component.rb +19 -0
  18. data/app/components/panda/core/admin/tab_bar_component.rb +101 -0
  19. data/app/components/panda/core/admin/table_component.rb +90 -9
  20. data/app/components/panda/core/admin/tag_component.rb +21 -16
  21. data/app/components/panda/core/admin/user_activity_component.rb +43 -0
  22. data/app/components/panda/core/admin/user_display_component.rb +78 -0
  23. data/app/components/panda/core/base.rb +122 -0
  24. data/app/controllers/panda/core/admin/base_controller.rb +68 -0
  25. data/app/controllers/panda/core/admin/dashboard_controller.rb +7 -6
  26. data/app/controllers/panda/core/admin/my_profile_controller.rb +3 -3
  27. data/app/controllers/panda/core/admin/sessions_controller.rb +26 -5
  28. data/app/helpers/panda/core/sessions_helper.rb +21 -0
  29. data/app/javascript/panda/core/application.js +1 -0
  30. data/app/javascript/panda/core/vendor/@hotwired--stimulus.js +4 -0
  31. data/app/javascript/panda/core/vendor/@hotwired--turbo.js +160 -0
  32. data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +4 -0
  33. data/app/models/panda/core/user.rb +17 -13
  34. data/app/views/layouts/panda/core/admin.html.erb +40 -57
  35. data/app/views/layouts/panda/core/admin_simple.html.erb +5 -0
  36. data/app/views/panda/core/admin/dashboard/_default_content.html.erb +73 -0
  37. data/app/views/panda/core/admin/dashboard/show.html.erb +4 -10
  38. data/app/views/panda/core/admin/my_profile/edit.html.erb +13 -27
  39. data/app/views/panda/core/admin/sessions/new.html.erb +13 -12
  40. data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +27 -34
  41. data/app/views/panda/core/admin/shared/_flash.html.erb +4 -30
  42. data/app/views/panda/core/admin/shared/_sidebar.html.erb +36 -20
  43. data/app/views/panda/core/shared/_footer.html.erb +2 -0
  44. data/app/views/panda/core/shared/_header.html.erb +19 -0
  45. data/config/importmap.rb +15 -0
  46. data/config/initializers/panda_core.rb +37 -1
  47. data/config/routes.rb +7 -7
  48. data/db/migrate/20250810120000_add_current_theme_to_panda_core_users.rb +7 -0
  49. data/lib/generators/panda/core/install_generator.rb +3 -9
  50. data/lib/generators/panda/core/templates/README +25 -0
  51. data/lib/generators/panda/core/templates/initializer.rb +28 -0
  52. data/lib/panda/core/asset_loader.rb +23 -8
  53. data/lib/panda/core/configuration.rb +41 -9
  54. data/lib/panda/core/debug.rb +47 -0
  55. data/lib/panda/core/engine.rb +82 -8
  56. data/lib/panda/core/version.rb +1 -1
  57. data/lib/panda/core.rb +1 -0
  58. data/lib/tasks/assets.rake +58 -392
  59. data/lib/tasks/panda_core_tasks.rake +16 -0
  60. metadata +102 -14
  61. data/app/components/panda/core/admin/container_component.html.erb +0 -12
  62. data/app/components/panda/core/admin/flash_message_component.html.erb +0 -31
  63. data/app/components/panda/core/admin/panel_component.html.erb +0 -7
  64. data/app/components/panda/core/admin/slideover_component.html.erb +0 -9
  65. data/app/components/panda/core/admin/table_component.html.erb +0 -29
  66. data/app/controllers/panda/core/admin_controller.rb +0 -28
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class StatisticsComponent < Panda::Core::Base
7
+ prop :metric, String
8
+ prop :value, _Nilable(_Union(String, Integer, Float))
9
+
10
+ def view_template
11
+ div(class: "overflow-hidden p-4 bg-gradient-to-br rounded-lg border-2 from-light/20 to-light border-mid") do
12
+ dt(class: "text-base font-medium truncate text-dark") { @metric }
13
+ dd(class: "mt-1 text-3xl font-medium tracking-tight text-dark") { @value }
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class TabBarComponent < Panda::Core::Base
7
+ prop :tabs, Array, default: -> { [].freeze }
8
+
9
+ def view_template
10
+ div(class: "mt-3 sm:mt-2") do
11
+ render_mobile_select
12
+ render_desktop_tabs
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def render_mobile_select
19
+ div(class: "sm:hidden") do
20
+ label(for: "tabs", class: "sr-only") { "Select a tab" }
21
+ select(
22
+ id: "tabs",
23
+ name: "tabs",
24
+ class: "block py-1.5 pr-10 pl-3 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset focus:ring-2 focus:ring-inset ring-mid focus:border-panda-dark focus:ring-panda-dark"
25
+ ) do
26
+ @tabs.each do |tab|
27
+ option { tab[:name] }
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def render_desktop_tabs
34
+ div(class: "hidden sm:block") do
35
+ div(class: "flex items-center border-b border-gray-200") do
36
+ nav(class: "flex flex-1 -mb-px space-x-6 xl:space-x-8", aria: {label: "Tabs"}) do
37
+ @tabs.each_with_index do |tab, index|
38
+ render_tab(tab, index.zero?)
39
+ end
40
+ end
41
+ render_view_toggle
42
+ end
43
+ end
44
+ end
45
+
46
+ def render_tab(tab, is_current = false)
47
+ classes = "py-4 px-1 text-sm font-medium whitespace-nowrap border-b-2 "
48
+ classes += if is_current || tab[:current]
49
+ "border-panda-dark text-panda-dark"
50
+ else
51
+ "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
52
+ end
53
+
54
+ a(
55
+ href: tab[:url] || "#",
56
+ class: classes,
57
+ aria: {current: (is_current || tab[:current]) ? "page" : nil}
58
+ ) { tab[:name] }
59
+ end
60
+
61
+ def render_view_toggle
62
+ div(class: "hidden items-center p-0.5 ml-6 bg-gray-100 rounded-lg sm:flex") do
63
+ render_view_button(:list)
64
+ render_view_button(:grid, selected: true)
65
+ end
66
+ end
67
+
68
+ def render_view_button(type, selected: false)
69
+ button_class = "p-1.5 text-gray-400 rounded-md focus:ring-2 focus:ring-inset focus:outline-none focus:ring-panda-dark"
70
+ button_class += if selected
71
+ " ml-0.5 bg-white shadow-sm"
72
+ else
73
+ " hover:bg-white hover:shadow-sm"
74
+ end
75
+
76
+ button(type: "button", class: button_class) do
77
+ if type == :list
78
+ svg(class: "w-5 h-5", viewBox: "0 0 20 20", fill: "currentColor", aria: {hidden: "true"}) do |s|
79
+ s.path(
80
+ fill_rule: "evenodd",
81
+ d: "M2 3.75A.75.75 0 012.75 3h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 3.75zm0 4.167a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75zm0 4.166a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75zm0 4.167a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75z",
82
+ clip_rule: "evenodd"
83
+ )
84
+ end
85
+ span(class: "sr-only") { "Use list view" }
86
+ else
87
+ svg(class: "w-5 h-5", viewBox: "0 0 20 20", fill: "currentColor", aria: {hidden: "true"}) do |s|
88
+ s.path(
89
+ fill_rule: "evenodd",
90
+ d: "M4.25 2A2.25 2.25 0 002 4.25v2.5A2.25 2.25 0 004.25 9h2.5A2.25 2.25 0 009 6.75v-2.5A2.25 2.25 0 006.75 2h-2.5zm0 9A2.25 2.25 0 002 13.25v2.5A2.25 2.25 0 004.25 18h2.5A2.25 2.25 0 009 15.75v-2.5A2.25 2.25 0 006.75 11h-2.5zm9-9A2.25 2.25 0 0011 4.25v2.5A2.25 2.25 0 0013.25 9h2.5A2.25 2.25 0 0018 6.75v-2.5A2.25 2.25 0 0015.75 2h-2.5zm0 9A2.25 2.25 0 0011 13.25v2.5A2.25 2.25 0 0013.25 18h2.5A2.25 2.25 0 0018 15.75v-2.5A2.25 2.25 0 0015.75 11h-2.5z",
91
+ clip_rule: "evenodd"
92
+ )
93
+ end
94
+ span(class: "sr-only") { "Use grid view" }
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -3,24 +3,105 @@
3
3
  module Panda
4
4
  module Core
5
5
  module Admin
6
- class TableComponent < ViewComponent::Base
6
+ class TableComponent < Panda::Core::Base
7
+ prop :term, String
8
+ prop :rows, _Nilable(Object), default: -> { [] }
9
+
7
10
  attr_reader :columns
8
11
 
9
- def initialize(term:, rows:)
10
- @term = term
11
- @rows = rows
12
+ def initialize(**props)
13
+ super
12
14
  @columns = []
13
15
  end
14
16
 
15
- def column(label, &)
16
- @columns << Column.new(label, &)
17
+ def view_template(&block)
18
+ # Capture the block to populate columns
19
+ instance_eval(&block) if block_given?
20
+
21
+ if @rows.any?
22
+ render_table_with_rows
23
+ else
24
+ render_empty_table
25
+ end
26
+ end
27
+
28
+ def column(label, &cell_block)
29
+ @columns << Column.new(label, &cell_block)
17
30
  end
18
31
 
19
32
  private
20
33
 
21
- # Ensures @columns gets populated [https://dev.to/rolandstuder/supercharged-table-component-built-with-viewcomponent-3j6i]
22
- def before_render
23
- content
34
+ def render_table_with_rows
35
+ div(class: "table overflow-x-auto mb-12 w-full rounded-lg border border-dark") do
36
+ render_header
37
+ render_rows
38
+ end
39
+ end
40
+
41
+ def render_empty_table
42
+ div(class: "table overflow-x-auto mb-12 w-full rounded-lg border border-dark") do
43
+ render_header
44
+ end
45
+
46
+ div(class: "text-center mx-12 block border border-dashed py-12 rounded-lg") do
47
+ h3(class: "py-1 text-xl font-semibold text-gray-900") { "No #{@term.pluralize}" }
48
+ p(class: "py-1 text-base text-gray-500") { "Get started by creating a new #{@term}." }
49
+ end
50
+ end
51
+
52
+ def render_header
53
+ div(class: "table-header-group") do
54
+ div(class: "table-row text-base font-medium text-white bg-dark") do
55
+ @columns.each_with_index do |column, i|
56
+ header_classes = "table-cell sticky top-0 z-10 p-4"
57
+ header_classes += " rounded-tl-md" if i.zero?
58
+ header_classes += " rounded-tr-md" if i == @columns.size - 1
59
+
60
+ div(class: header_classes) { column.label }
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ def render_rows
67
+ div(class: "table-row-group") do
68
+ @rows.each do |row|
69
+ div(
70
+ class: "table-row relative bg-gray-500/5 hover:bg-gray-500/20",
71
+ data: {post_id: row.id}
72
+ ) do
73
+ @columns.each do |column|
74
+ div(class: "table-cell py-5 px-3 h-20 text-sm align-middle whitespace-nowrap border-b border-dark/20") do
75
+ # Capture the cell content by calling the block with the row
76
+ render_cell_content(row, column.cell)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ def render_cell_content(row, cell_block)
85
+ # When called from ERB, we need to capture the block's output buffer
86
+ # When called from Phlex, evaluate directly
87
+ if defined?(view_context) && view_context
88
+ # Use capture to get ERB output buffer content
89
+ captured_html = view_context.capture(row, &cell_block)
90
+ # Render the captured HTML (already html_safe from capture)
91
+ raw(captured_html)
92
+ else
93
+ # Pure Phlex context - execute block directly
94
+ result = cell_block.call(row)
95
+
96
+ # Handle different return types
97
+ if result.is_a?(String)
98
+ plain(result)
99
+ elsif result.respond_to?(:render_in)
100
+ render(result)
101
+ else
102
+ plain(result.to_s)
103
+ end
104
+ end
24
105
  end
25
106
  end
26
107
 
@@ -3,30 +3,35 @@
3
3
  module Panda
4
4
  module Core
5
5
  module Admin
6
- class TagComponent < ViewComponent::Base
7
- attr_accessor :status, :text
6
+ class TagComponent < Panda::Core::Base
7
+ prop :status, Symbol, default: :active
8
+ prop :text, _Nilable(String), default: -> {}
8
9
 
9
- def initialize(status: :active, text: nil)
10
- @status = status.to_sym
11
- @text = text || status.to_s.humanize
10
+ def view_template
11
+ span(class: tag_classes) { computed_text }
12
12
  end
13
13
 
14
- def call
15
- classes = "inline-flex items-center py-1 px-2 text-xs font-medium rounded-md ring-1 ring-inset "
14
+ private
16
15
 
17
- classes += case @status
16
+ def computed_text
17
+ @text || @status.to_s.humanize
18
+ end
19
+
20
+ def tag_classes
21
+ base = "inline-flex items-center py-1 px-2 text-xs font-medium rounded-md ring-1 ring-inset "
22
+ base + status_classes
23
+ end
24
+
25
+ def status_classes
26
+ case @status
18
27
  when :active
19
- "text-white ring-black/30 bg-green-600 border-0 "
28
+ "text-white ring-black/30 bg-green-600 border-0"
20
29
  when :draft
21
- "text-black ring-black/30 bg-yellow-400 "
30
+ "text-black ring-black/30 bg-yellow-400"
22
31
  when :inactive, :hidden
23
- "text-black ring-black/30 bg-black/5 bg-white "
32
+ "text-black ring-black/30 bg-black/5 bg-white"
24
33
  else
25
- "text-black bg-white "
26
- end
27
-
28
- content_tag :span, class: classes do
29
- @text
34
+ "text-black bg-white"
30
35
  end
31
36
  end
32
37
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class UserActivityComponent < Panda::Core::Base
7
+ include ActionView::Helpers::DateHelper
8
+
9
+ prop :model, _Nilable(Object), default: -> {}
10
+ prop :at, _Nilable(Object), default: -> {}
11
+ prop :user, _Nilable(Object), default: -> {}
12
+
13
+ def view_template
14
+ return unless should_render?
15
+
16
+ if @user.is_a?(Panda::Core::User) && time
17
+ render Panda::Core::Admin::UserDisplayComponent.new(
18
+ user: @user,
19
+ metadata: "#{time_ago_in_words(time)} ago"
20
+ )
21
+ elsif @user.is_a?(Panda::Core::User)
22
+ render Panda::Core::Admin::UserDisplayComponent.new(
23
+ user: @user,
24
+ metadata: "Not published"
25
+ )
26
+ elsif time
27
+ div(class: "text-black/60") { "#{time_ago_in_words(time)} ago" }
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def time
34
+ @at if @at.is_a?(ActiveSupport::TimeWithZone)
35
+ end
36
+
37
+ def should_render?
38
+ @user.is_a?(Panda::Core::User) || time
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class UserDisplayComponent < Panda::Core::Base
7
+ prop :user_id, _Nilable(String), default: -> {}
8
+ prop :user, _Nilable(Object), default: -> {}
9
+ prop :metadata, String, default: ""
10
+
11
+ def view_template
12
+ return unless resolved_user
13
+
14
+ div(class: "block flex-shrink-0 group") do
15
+ div(class: "flex items-center") do
16
+ render_avatar
17
+ render_user_info
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def resolved_user
25
+ @resolved_user ||= if @user.nil? && @user_id.present?
26
+ Panda::Core::User.find_by(id: @user_id)
27
+ else
28
+ @user
29
+ end
30
+ end
31
+
32
+ def render_avatar
33
+ has_image = resolved_user.respond_to?(:image_url) &&
34
+ resolved_user.image_url.present? &&
35
+ !resolved_user.image_url.empty?
36
+
37
+ if has_image
38
+ div do
39
+ img(
40
+ class: "inline-block w-10 h-10 rounded-full",
41
+ src: resolved_user.image_url,
42
+ alt: ""
43
+ )
44
+ end
45
+ else
46
+ div(class: "inline-block w-10 h-10 bg-gray-200 rounded-full flex items-center justify-center") do
47
+ span(class: "text-sm font-medium text-gray-600") { user_initials }
48
+ end
49
+ end
50
+ end
51
+
52
+ def user_initials
53
+ return "" unless resolved_user.respond_to?(:name)
54
+
55
+ name_parts = resolved_user.name.to_s.split
56
+ if name_parts.length >= 2
57
+ "#{name_parts.first[0]}#{name_parts.last[0]}".upcase
58
+ elsif name_parts.length == 1
59
+ name_parts.first[0..1].upcase
60
+ else
61
+ ""
62
+ end
63
+ end
64
+
65
+ def render_user_info
66
+ div(class: "ml-3") do
67
+ p(class: "text-sm text-black") { resolved_user.name }
68
+ if @metadata.present?
69
+ p(class: "text-sm text-black/60") { @metadata }
70
+ elsif resolved_user.respond_to?(:email) && resolved_user.email.present?
71
+ p(class: "text-sm text-gray-500") { resolved_user.email }
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ # Base class for all Phlex components in the Panda ecosystem.
6
+ #
7
+ # This base component provides:
8
+ # - Type-safe properties via Literal
9
+ # - Tailwind CSS class merging
10
+ # - Attribute merging with sensible defaults
11
+ # - Rails helper integration
12
+ # - Development-mode debugging comments
13
+ #
14
+ # @example Basic usage
15
+ # class MyComponent < Panda::Core::Base
16
+ # prop :title, String
17
+ # prop :variant, Symbol, default: :primary
18
+ #
19
+ # def view_template
20
+ # div(**@attrs) { title }
21
+ # end
22
+ #
23
+ # def default_attrs
24
+ # { class: "my-component my-component--#{variant}" }
25
+ # end
26
+ # end
27
+ #
28
+ # @example With attribute merging
29
+ # # Component definition
30
+ # class Button < Panda::Core::Base
31
+ # prop :text, String
32
+ #
33
+ # def view_template
34
+ # button(**@attrs) { text }
35
+ # end
36
+ #
37
+ # def default_attrs
38
+ # { class: "btn btn-primary", type: "button" }
39
+ # end
40
+ # end
41
+ #
42
+ # # Usage - user attrs merge with defaults
43
+ # render Button.new(text: "Click me", class: "mt-4", type: "submit")
44
+ # # => <button type="submit" class="btn btn-primary mt-4">Click me</button>
45
+ #
46
+ class Base < Phlex::HTML
47
+ # Frozen instance of TailwindMerge for efficient class merging
48
+ TAILWIND_MERGER = ::TailwindMerge::Merger.new.freeze unless defined?(TAILWIND_MERGER)
49
+
50
+ # Enable type-safe properties via Literal
51
+ extend Literal::Properties
52
+
53
+ # Include Rails helpers for routes, etc.
54
+ include Phlex::Rails::Helpers::Routes
55
+
56
+ # Special handling for the attrs property - merges user attributes with defaults
57
+ # and intelligently handles Tailwind class merging
58
+ #
59
+ # @param value [Hash] User-provided attributes
60
+ # @return [Hash] Merged attributes with Tailwind classes properly combined
61
+ prop :attrs, Hash, :**, reader: :private do |value|
62
+ merge_attrs(value, default_attrs)
63
+ end
64
+
65
+ # Merges user-provided attributes with default attributes.
66
+ # Special handling for :class to merge Tailwind classes intelligently.
67
+ #
68
+ # @param user_attrs [Hash] Attributes provided by the user
69
+ # @param default_attrs [Hash] Default attributes from the component
70
+ # @return [Hash] Merged attributes
71
+ def merge_attrs(user_attrs, default_attrs)
72
+ attrs = default_attrs.merge(user_attrs)
73
+ if attrs[:class].is_a?(String)
74
+ attrs[:class] = TAILWIND_MERGER.merge(attrs[:class])
75
+ end
76
+ attrs
77
+ end
78
+
79
+ # Helper alias for merge_attrs with clearer intent
80
+ #
81
+ # @param user_attrs [Hash] Attributes provided by the user
82
+ # @param default_attrs [Hash] Default attributes from the component
83
+ # @return [Hash] Merged attributes with Tailwind classes combined
84
+ def tailwind_merge_attrs(user_attrs, default_attrs)
85
+ merge_attrs(user_attrs, default_attrs)
86
+ end
87
+
88
+ # Override this method in subclasses to provide default attributes
89
+ # for your component.
90
+ #
91
+ # @return [Hash] Default HTML attributes for the component
92
+ #
93
+ # @example
94
+ # def default_attrs
95
+ # {
96
+ # class: "btn btn-#{variant}",
97
+ # type: "button",
98
+ # data: { controller: "button" }
99
+ # }
100
+ # end
101
+ def default_attrs
102
+ {}
103
+ end
104
+
105
+ # In development mode, wrap components with HTML comments
106
+ # showing their class name for easier debugging
107
+ if Rails.env.development?
108
+ def before_template
109
+ class_name = self.class.name
110
+ comment { "Begin #{class_name}" }
111
+ super
112
+ end
113
+
114
+ def after_template
115
+ class_name = self.class.name
116
+ super
117
+ comment { "End #{class_name}" }
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ # Base controller for all admin interfaces across Panda gems
7
+ # Provides authentication, helpers, and hooks for extending functionality
8
+ class BaseController < ::ActionController::Base
9
+ layout "panda/core/admin"
10
+
11
+ protect_from_forgery with: :exception
12
+
13
+ # Add flash types for improved alert support with Tailwind
14
+ add_flash_types :success, :warning, :error, :info
15
+
16
+ # Include helper modules
17
+ helper Panda::Core::SessionsHelper
18
+ helper Panda::Core::AssetHelper if defined?(Panda::Core::AssetHelper)
19
+
20
+ before_action :set_current_request_details
21
+ before_action :authenticate_admin_user!
22
+
23
+ helper_method :breadcrumbs
24
+ helper_method :current_user
25
+ helper_method :user_signed_in?
26
+
27
+ def breadcrumbs
28
+ @breadcrumbs ||= []
29
+ end
30
+
31
+ def add_breadcrumb(name, path = nil)
32
+ breadcrumbs << Breadcrumb.new(name, path)
33
+ end
34
+
35
+ # Set the current request details
36
+ # @return [void]
37
+ def set_current_request_details
38
+ # Set Core current attributes
39
+ Panda::Core::Current.request_id = request.uuid
40
+ Panda::Core::Current.user_agent = request.user_agent
41
+ Panda::Core::Current.ip_address = request.ip
42
+ Panda::Core::Current.root = request.base_url
43
+ Panda::Core::Current.user ||= Panda::Core::User.find_by(id: session[:user_id]) if session[:user_id]
44
+ end
45
+
46
+ def authenticate_user!
47
+ redirect_to main_app.root_path, flash: {error: "Please login to view this!"} unless user_signed_in?
48
+ end
49
+
50
+ def authenticate_admin_user!
51
+ return if user_signed_in? && current_user.admin?
52
+
53
+ redirect_to panda_core.admin_login_path,
54
+ flash: {error: "Please login to view this!"}
55
+ end
56
+
57
+ # Required for paper_trail and seems as good as convention these days
58
+ def current_user
59
+ Panda::Core::Current.user
60
+ end
61
+
62
+ def user_signed_in?
63
+ !!Panda::Core::Current.user
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -3,17 +3,18 @@
3
3
  module Panda
4
4
  module Core
5
5
  module Admin
6
- class DashboardController < AdminController
6
+ class DashboardController < BaseController
7
7
  # Authentication is automatically enforced by AdminController
8
8
 
9
9
  def show
10
10
  # If a custom dashboard path is configured, redirect there
11
- if Panda::Core.configuration.dashboard_redirect_path
12
- redirect_to Panda::Core.configuration.dashboard_redirect_path
11
+ if Panda::Core.config.dashboard_redirect_path
12
+ redirect_path = Panda::Core.config.dashboard_redirect_path
13
+ redirect_path = redirect_path.call if redirect_path.respond_to?(:call)
14
+ redirect_to redirect_path
13
15
  else
14
- # This can be overridden by applications using panda-core
15
- # For now, just render a basic view
16
- render plain: "Welcome to Panda Admin"
16
+ # Render the dashboard view
17
+ render :show
17
18
  end
18
19
  end
19
20
  end
@@ -3,7 +3,7 @@
3
3
  module Panda
4
4
  module Core
5
5
  module Admin
6
- class MyProfileController < ::Panda::Core::AdminController
6
+ class MyProfileController < BaseController
7
7
  before_action :set_initial_breadcrumb, only: %i[edit update]
8
8
 
9
9
  # Shows the edit form for the current user's profile
@@ -36,10 +36,10 @@ module Panda
36
36
  # @return ActionController::StrongParameters
37
37
  def user_params
38
38
  # Base parameters that Core always allows
39
- base_params = [:firstname, :lastname, :email, :current_theme]
39
+ base_params = [:name, :email, :current_theme]
40
40
 
41
41
  # Allow additional params from configuration
42
- additional_params = Core.configuration.additional_user_params || []
42
+ additional_params = Core.config.additional_user_params || []
43
43
 
44
44
  params.require(:user).permit(*(base_params + additional_params))
45
45
  end