panda-cms 0.7.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.
- checksums.yaml +7 -0
- data/README.md +73 -0
- data/Rakefile +7 -0
- data/app/assets/builds/panda.cms.css +2808 -0
- data/app/assets/config/panda_cms_manifest.js +4 -0
- data/app/assets/stylesheets/panda/cms/application.tailwind.css +162 -0
- data/app/assets/stylesheets/panda/cms/editor.css +120 -0
- data/app/builders/panda/cms/form_builder.rb +234 -0
- data/app/components/panda/cms/admin/button_component.rb +70 -0
- data/app/components/panda/cms/admin/container_component.html.erb +13 -0
- data/app/components/panda/cms/admin/container_component.rb +13 -0
- data/app/components/panda/cms/admin/flash_message_component.html.erb +31 -0
- data/app/components/panda/cms/admin/flash_message_component.rb +47 -0
- data/app/components/panda/cms/admin/heading_component.rb +45 -0
- data/app/components/panda/cms/admin/panel_component.html.erb +7 -0
- data/app/components/panda/cms/admin/panel_component.rb +13 -0
- data/app/components/panda/cms/admin/slideover_component.html.erb +9 -0
- data/app/components/panda/cms/admin/slideover_component.rb +15 -0
- data/app/components/panda/cms/admin/statistics_component.html.erb +4 -0
- data/app/components/panda/cms/admin/statistics_component.rb +17 -0
- data/app/components/panda/cms/admin/tab_bar_component.html.erb +35 -0
- data/app/components/panda/cms/admin/tab_bar_component.rb +15 -0
- data/app/components/panda/cms/admin/table_component.html.erb +29 -0
- data/app/components/panda/cms/admin/table_component.rb +46 -0
- data/app/components/panda/cms/admin/tag_component.rb +35 -0
- data/app/components/panda/cms/admin/user_activity_component.html.erb +5 -0
- data/app/components/panda/cms/admin/user_activity_component.rb +33 -0
- data/app/components/panda/cms/admin/user_display_component.html.erb +17 -0
- data/app/components/panda/cms/admin/user_display_component.rb +21 -0
- data/app/components/panda/cms/code_component.rb +64 -0
- data/app/components/panda/cms/grid_component.html.erb +6 -0
- data/app/components/panda/cms/grid_component.rb +15 -0
- data/app/components/panda/cms/menu_component.html.erb +6 -0
- data/app/components/panda/cms/menu_component.rb +58 -0
- data/app/components/panda/cms/page_menu_component.html.erb +21 -0
- data/app/components/panda/cms/page_menu_component.rb +38 -0
- data/app/components/panda/cms/rich_text_component.html.erb +6 -0
- data/app/components/panda/cms/rich_text_component.rb +84 -0
- data/app/components/panda/cms/text_component.rb +72 -0
- data/app/constraints/panda/cms/admin_constraint.rb +18 -0
- data/app/controllers/panda/cms/admin/block_contents_controller.rb +52 -0
- data/app/controllers/panda/cms/admin/dashboard_controller.rb +20 -0
- data/app/controllers/panda/cms/admin/files_controller.rb +21 -0
- data/app/controllers/panda/cms/admin/forms_controller.rb +53 -0
- data/app/controllers/panda/cms/admin/menus_controller.rb +30 -0
- data/app/controllers/panda/cms/admin/pages_controller.rb +91 -0
- data/app/controllers/panda/cms/admin/posts_controller.rb +146 -0
- data/app/controllers/panda/cms/admin/sessions_controller.rb +94 -0
- data/app/controllers/panda/cms/admin/settings/bulk_editor_controller.rb +37 -0
- data/app/controllers/panda/cms/admin/settings_controller.rb +20 -0
- data/app/controllers/panda/cms/application_controller.rb +57 -0
- data/app/controllers/panda/cms/errors_controller.rb +33 -0
- data/app/controllers/panda/cms/form_submissions_controller.rb +23 -0
- data/app/controllers/panda/cms/pages_controller.rb +72 -0
- data/app/controllers/panda/cms/posts_controller.rb +13 -0
- data/app/helpers/panda/cms/admin/files_helper.rb +6 -0
- data/app/helpers/panda/cms/admin/pages_helper.rb +6 -0
- data/app/helpers/panda/cms/admin/posts_helper.rb +48 -0
- data/app/helpers/panda/cms/application_helper.rb +120 -0
- data/app/helpers/panda/cms/pages_helper.rb +6 -0
- data/app/helpers/panda/cms/theme_helper.rb +18 -0
- data/app/javascript/panda/cms/@editorjs--editorjs.js +2577 -0
- data/app/javascript/panda/cms/@hotwired--stimulus.js +4 -0
- data/app/javascript/panda/cms/@hotwired--turbo.js +160 -0
- data/app/javascript/panda/cms/@rails--actioncable--src.js +4 -0
- data/app/javascript/panda/cms/application_panda_cms.js +39 -0
- data/app/javascript/panda/cms/controllers/dashboard_controller.js +7 -0
- data/app/javascript/panda/cms/controllers/editor_form_controller.js +77 -0
- data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +320 -0
- data/app/javascript/panda/cms/controllers/index.js +48 -0
- data/app/javascript/panda/cms/controllers/slug_controller.js +87 -0
- data/app/javascript/panda/cms/editor/css_extractor.js +80 -0
- data/app/javascript/panda/cms/editor/editor_js_config.js +177 -0
- data/app/javascript/panda/cms/editor/editor_js_initializer.js +285 -0
- data/app/javascript/panda/cms/editor/plain_text_editor.js +110 -0
- data/app/javascript/panda/cms/editor/resource_loader.js +115 -0
- data/app/javascript/panda/cms/tailwindcss-stimulus-components.js +4 -0
- data/app/jobs/panda/cms/application_job.rb +6 -0
- data/app/jobs/panda/cms/record_visit_job.rb +31 -0
- data/app/mailers/panda/cms/application_mailer.rb +8 -0
- data/app/mailers/panda/cms/form_mailer.rb +21 -0
- data/app/models/action_text/rich_text_version.rb +6 -0
- data/app/models/panda/cms/application_record.rb +7 -0
- data/app/models/panda/cms/block.rb +34 -0
- data/app/models/panda/cms/block_content.rb +18 -0
- data/app/models/panda/cms/block_content_version.rb +8 -0
- data/app/models/panda/cms/breadcrumb.rb +12 -0
- data/app/models/panda/cms/current.rb +17 -0
- data/app/models/panda/cms/form.rb +9 -0
- data/app/models/panda/cms/form_submission.rb +7 -0
- data/app/models/panda/cms/menu.rb +52 -0
- data/app/models/panda/cms/menu_item.rb +58 -0
- data/app/models/panda/cms/page.rb +96 -0
- data/app/models/panda/cms/page_version.rb +8 -0
- data/app/models/panda/cms/post.rb +60 -0
- data/app/models/panda/cms/post_version.rb +8 -0
- data/app/models/panda/cms/redirect.rb +11 -0
- data/app/models/panda/cms/template.rb +124 -0
- data/app/models/panda/cms/template_version.rb +8 -0
- data/app/models/panda/cms/user.rb +31 -0
- data/app/models/panda/cms/version.rb +8 -0
- data/app/models/panda/cms/visit.rb +9 -0
- data/app/services/panda/cms/html_to_editor_js_converter.rb +200 -0
- data/app/views/active_storage/blobs/blobs/_blob.html.erb +14 -0
- data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
- data/app/views/layouts/panda/cms/application.html.erb +41 -0
- data/app/views/layouts/panda/cms/public.html.erb +3 -0
- data/app/views/panda/cms/admin/dashboard/show.html.erb +12 -0
- data/app/views/panda/cms/admin/files/index.html.erb +124 -0
- data/app/views/panda/cms/admin/files/show.html.erb +2 -0
- data/app/views/panda/cms/admin/forms/edit.html.erb +0 -0
- data/app/views/panda/cms/admin/forms/index.html.erb +13 -0
- data/app/views/panda/cms/admin/forms/new.html.erb +15 -0
- data/app/views/panda/cms/admin/forms/show.html.erb +35 -0
- data/app/views/panda/cms/admin/menus/index.html.erb +8 -0
- data/app/views/panda/cms/admin/pages/edit.html.erb +36 -0
- data/app/views/panda/cms/admin/pages/index.html.erb +22 -0
- data/app/views/panda/cms/admin/pages/new.html.erb +15 -0
- data/app/views/panda/cms/admin/pages/show.html.erb +1 -0
- data/app/views/panda/cms/admin/posts/_form.html.erb +29 -0
- data/app/views/panda/cms/admin/posts/edit.html.erb +6 -0
- data/app/views/panda/cms/admin/posts/index.html.erb +18 -0
- data/app/views/panda/cms/admin/posts/new.html.erb +6 -0
- data/app/views/panda/cms/admin/sessions/new.html.erb +17 -0
- data/app/views/panda/cms/admin/settings/bulk_editor/new.html.erb +68 -0
- data/app/views/panda/cms/admin/settings/index.html.erb +21 -0
- data/app/views/panda/cms/admin/settings/insta.html +4 -0
- data/app/views/panda/cms/admin/shared/_breadcrumbs.html.erb +28 -0
- data/app/views/panda/cms/admin/shared/_flash.html.erb +5 -0
- data/app/views/panda/cms/admin/shared/_sidebar.html.erb +41 -0
- data/app/views/panda/cms/form_mailer/notification_email.html.erb +11 -0
- data/app/views/panda/cms/shared/_editor.html.erb +0 -0
- data/app/views/panda/cms/shared/_favicons.html.erb +9 -0
- data/app/views/panda/cms/shared/_footer.html.erb +2 -0
- data/app/views/panda/cms/shared/_header.html.erb +15 -0
- data/app/views/panda/cms/shared/_importmap.html.erb +33 -0
- data/config/importmap.rb +13 -0
- data/config/initializers/inflections.rb +3 -0
- data/config/initializers/panda/cms/form_errors.rb +38 -0
- data/config/initializers/panda/cms/healthcheck_log_silencer.rb +11 -0
- data/config/initializers/panda/cms/paper_trail.rb +7 -0
- data/config/initializers/panda/cms.rb +10 -0
- data/config/initializers/zeitwork.rb +3 -0
- data/config/locales/en.yml +49 -0
- data/config/puma/test.rb +9 -0
- data/config/routes.rb +48 -0
- data/config/tailwind.config.js +37 -0
- data/db/migrate/20240205223709_create_panda_cms_pages.rb +9 -0
- data/db/migrate/20240219213327_create_panda_cms_page_versions.rb +14 -0
- data/db/migrate/20240303002805_create_panda_cms_templates.rb +11 -0
- data/db/migrate/20240303003434_create_panda_cms_template_versions.rb +14 -0
- data/db/migrate/20240303022441_create_panda_cms_blocks.rb +13 -0
- data/db/migrate/20240303024256_create_panda_cms_block_contents.rb +10 -0
- data/db/migrate/20240303024746_create_panda_cms_block_content_versions.rb +14 -0
- data/db/migrate/20240303233238_add_panda_cms_menu_table.rb +10 -0
- data/db/migrate/20240303234724_add_panda_cms_menu_item_table.rb +12 -0
- data/db/migrate/20240304134343_add_parent_id_to_panda_cms_pages.rb +5 -0
- data/db/migrate/20240305000000_convert_html_content_to_editor_js.rb +82 -0
- data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +9 -0
- data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +16 -0
- data/db/migrate/20240316212822_add_kind_to_panda_cms_menus.rb +6 -0
- data/db/migrate/20240316221425_add_start_page_to_panda_cms_menus.rb +5 -0
- data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +24 -0
- data/db/migrate/20240317010532_create_panda_cms_users.rb +12 -0
- data/db/migrate/20240317161534_add_max_uses_to_panda_cms_template.rb +7 -0
- data/db/migrate/20240317163053_reset_counter_cache_on_panda_cms_template.rb +5 -0
- data/db/migrate/20240317214827_create_panda_cms_redirects.rb +14 -0
- data/db/migrate/20240317230622_create_panda_cms_visits.rb +13 -0
- data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +58 -0
- data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +5 -0
- data/db/migrate/20240701225422_add_service_name_to_active_storage_blobs.active_storage.rb +22 -0
- data/db/migrate/20240701225423_create_active_storage_variant_records.active_storage.rb +28 -0
- data/db/migrate/20240701225424_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +8 -0
- data/db/migrate/20240804235210_create_panda_cms_forms.rb +11 -0
- data/db/migrate/20240805013612_create_panda_cms_form_submissions.rb +9 -0
- data/db/migrate/20240805121123_create_panda_cms_posts.rb +27 -0
- data/db/migrate/20240805123104_create_panda_cms_post_versions.rb +14 -0
- data/db/migrate/20240806112735_fix_panda_cms_visits_column_names.rb +13 -0
- data/db/migrate/20240806204412_add_completion_path_to_panda_cms_forms.rb +5 -0
- data/db/migrate/20240820081917_change_form_submissions_to_submission_count.rb +5 -0
- data/db/migrate/20240904200605_create_action_text_tables.action_text.rb +24 -0
- data/db/migrate/20240923234535_add_depth_to_panda_cms_menus.rb +11 -0
- data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +5 -0
- data/db/migrate/20241119214548_convert_post_content_to_editor_js.rb +35 -0
- data/db/migrate/20241119214549_remove_action_text_from_posts.rb +9 -0
- data/db/migrate/20241120000419_remove_post_tag_references.rb +19 -0
- data/db/migrate/20241120110943_add_editor_js_to_posts.rb +27 -0
- data/db/migrate/20241120113859_add_cached_content_to_panda_cms_posts.rb +5 -0
- data/db/migrate/20241123234140_remove_post_tag_id_from_posts.rb +5 -0
- data/db/migrate/migrate +1 -0
- data/db/seeds.rb +5 -0
- data/lib/generators/panda/cms/install_generator.rb +29 -0
- data/lib/panda/cms/bulk_editor.rb +171 -0
- data/lib/panda/cms/demo_site_generator.rb +67 -0
- data/lib/panda/cms/editor_js/blocks/alert.rb +34 -0
- data/lib/panda/cms/editor_js/blocks/base.rb +33 -0
- data/lib/panda/cms/editor_js/blocks/header.rb +15 -0
- data/lib/panda/cms/editor_js/blocks/image.rb +36 -0
- data/lib/panda/cms/editor_js/blocks/list.rb +32 -0
- data/lib/panda/cms/editor_js/blocks/paragraph.rb +15 -0
- data/lib/panda/cms/editor_js/blocks/quote.rb +41 -0
- data/lib/panda/cms/editor_js/blocks/table.rb +50 -0
- data/lib/panda/cms/editor_js/renderer.rb +124 -0
- data/lib/panda/cms/editor_js.rb +16 -0
- data/lib/panda/cms/editor_js_content.rb +21 -0
- data/lib/panda/cms/engine.rb +257 -0
- data/lib/panda/cms/exceptions_app.rb +26 -0
- data/lib/panda/cms/railtie.rb +11 -0
- data/lib/panda/cms/slug.rb +24 -0
- data/lib/panda/cms.rb +0 -0
- data/lib/panda-cms/version.rb +5 -0
- data/lib/panda-cms.rb +81 -0
- data/lib/tasks/panda_cms.rake +54 -0
- data/lib/templates/erb/scaffold/_form.html.erb.tt +43 -0
- data/lib/templates/erb/scaffold/edit.html.erb.tt +8 -0
- data/lib/templates/erb/scaffold/index.html.erb.tt +14 -0
- data/lib/templates/erb/scaffold/new.html.erb.tt +7 -0
- data/lib/templates/erb/scaffold/partial.html.erb.tt +22 -0
- data/lib/templates/erb/scaffold/show.html.erb.tt +15 -0
- 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 +9 -0
- 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 +61 -0
- data/public/panda-cms-assets/favicons/site.webmanifest +14 -0
- 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 +568 -0
- metadata +654 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
module Admin
|
|
6
|
+
class HeadingComponent < ViewComponent::Base
|
|
7
|
+
renders_many :buttons, Panda::CMS::Admin::ButtonComponent
|
|
8
|
+
|
|
9
|
+
attr_reader :text, :level, :icon, :additional_styles
|
|
10
|
+
|
|
11
|
+
def initialize(text:, level: 2, icon: "", additional_styles: "")
|
|
12
|
+
@text = text
|
|
13
|
+
@level = level
|
|
14
|
+
@icon = icon
|
|
15
|
+
@additional_styles = additional_styles
|
|
16
|
+
@additional_styles = @additional_styles.split(" ") if @additional_styles.is_a?(String)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
output = ""
|
|
21
|
+
output += content_tag(:div, @text, class: "grow")
|
|
22
|
+
|
|
23
|
+
if buttons?
|
|
24
|
+
output += content_tag(:span, class: "actions flex gap-x-2 -mt-1") do
|
|
25
|
+
safe_join(buttons, "")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
output = output.html_safe
|
|
30
|
+
base_heading_styles = "flex pt-1 text-black mb-5 -mt-1"
|
|
31
|
+
|
|
32
|
+
if level == 1
|
|
33
|
+
content_tag(:h1, output, class: [base_heading_styles, "text-2xl font-medium", @additional_styles])
|
|
34
|
+
elsif level == 2
|
|
35
|
+
content_tag(:h2, output, class: [base_heading_styles, "text-xl font-medium", @additional_styles])
|
|
36
|
+
elsif level == 3
|
|
37
|
+
content_tag(:h3, output, class: [base_heading_styles, "text-xl", "font-light", @additional_styles])
|
|
38
|
+
elsif level == :panel
|
|
39
|
+
content_tag(:h3, output, class: ["text-base font-medium p-4 text-white"])
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
module Admin
|
|
6
|
+
class PanelComponent < ViewComponent::Base
|
|
7
|
+
renders_one :heading, ->(text:, icon: "", level: :panel, additional_styles: "") do
|
|
8
|
+
Panda::CMS::Admin::HeadingComponent.new(text: text, icon: icon, level: level, additional_styles: additional_styles)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
module Admin
|
|
6
|
+
class StatisticsComponent < ViewComponent::Base
|
|
7
|
+
attr_reader :metric
|
|
8
|
+
attr_reader :value
|
|
9
|
+
|
|
10
|
+
def initialize(metric:, value:)
|
|
11
|
+
@metric = metric
|
|
12
|
+
@value = value
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<div class="mt-3 sm:mt-2">
|
|
2
|
+
<div class="sm:hidden">
|
|
3
|
+
<label for="tabs" class="sr-only">Select a tab</label>
|
|
4
|
+
<!-- Use an "onChange" listener to redirect the user to the selected tab URL. -->
|
|
5
|
+
<select id="tabs" name="tabs" 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">
|
|
6
|
+
<% tabs.each do |tab| %>
|
|
7
|
+
<option><%= tab %></option>
|
|
8
|
+
<% end %>
|
|
9
|
+
</select>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="hidden sm:block">
|
|
12
|
+
<div class="flex items-center border-b border-gray-200">
|
|
13
|
+
<nav class="flex flex-1 -mb-px space-x-6 xl:space-x-8" aria-label="Tabs">
|
|
14
|
+
<!-- Current: "border-panda-dark text-panda-dark", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" -->
|
|
15
|
+
<a href="#" aria-current="page" class="py-4 px-1 text-sm font-medium whitespace-nowrap border-b-2 border-panda-dark text-panda-dark">Recently Viewed</a>
|
|
16
|
+
<a href="#" class="py-4 px-1 text-sm font-medium text-gray-500 whitespace-nowrap border-b-2 border-transparent hover:text-gray-700 hover:border-gray-300">Recently Added</a>
|
|
17
|
+
<a href="#" class="py-4 px-1 text-sm font-medium text-gray-500 whitespace-nowrap border-b-2 border-transparent hover:text-gray-700 hover:border-gray-300">Favourited</a>
|
|
18
|
+
</nav>
|
|
19
|
+
<div class="hidden items-center p-0.5 ml-6 bg-gray-100 rounded-lg sm:flex">
|
|
20
|
+
<button type="button" class="p-1.5 text-gray-400 rounded-md hover:bg-white hover:shadow-sm focus:ring-2 focus:ring-inset focus:outline-none focus:ring-panda-dark">
|
|
21
|
+
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
22
|
+
<path fill-rule="evenodd" 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" clip-rule="evenodd" />
|
|
23
|
+
</svg>
|
|
24
|
+
<span class="sr-only">Use list view</span>
|
|
25
|
+
</button>
|
|
26
|
+
<button type="button" class="p-1.5 ml-0.5 text-gray-400 bg-white rounded-md shadow-sm focus:ring-2 focus:ring-inset focus:outline-none focus:ring-panda-dark">
|
|
27
|
+
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
28
|
+
<path fill-rule="evenodd" 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" clip-rule="evenodd" />
|
|
29
|
+
</svg>
|
|
30
|
+
<span class="sr-only">Use grid view</span>
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<div class="table overflow-x-auto mb-12 w-full rounded-lg border border-mid">
|
|
2
|
+
<div class="table-header-group">
|
|
3
|
+
<div class="table-row text-base font-medium text-white bg-mid">
|
|
4
|
+
<% columns.each_with_index do |column, i| %>
|
|
5
|
+
<div class="table-cell sticky top-0 z-10 p-4 <% if i.zero? %>rounded-tl-md<% elsif i == columns.size - 1 %>rounded-tr-md<% end %>"><%= column.label %></div>
|
|
6
|
+
<% end %>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<% if @rows.any? %>
|
|
11
|
+
<div class="table-row-group">
|
|
12
|
+
<% @rows.each do |row| %>
|
|
13
|
+
<div class="table-row relative bg-mid/5 hover:bg-mid/20">
|
|
14
|
+
<% @columns.each do |column| %>
|
|
15
|
+
<div class="table-cell py-5 px-3 h-20 text-sm align-middle whitespace-nowrap border-b border-mid/20">
|
|
16
|
+
<%= view_context.capture(row, &column.cell) %>
|
|
17
|
+
</div>
|
|
18
|
+
<% end %>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<% else %>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="text-center mx-12 block border border-dashed py-12 rounded-lg">
|
|
26
|
+
<h3 class="py-1 text-xl font-semibold text-gray-900">No <%= @term.pluralize %></h3>
|
|
27
|
+
<p class="py-1 text-base text-gray-500">Get started by creating a new <%= @term %>.</p>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
module Admin
|
|
6
|
+
class TableComponent < ViewComponent::Base
|
|
7
|
+
attr_reader :columns
|
|
8
|
+
|
|
9
|
+
def initialize(term:, rows:)
|
|
10
|
+
@term = term
|
|
11
|
+
@rows = rows
|
|
12
|
+
@columns = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def column(label, &)
|
|
16
|
+
@columns << Column.new(label, &)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# Ensures @columns gets populated [https://dev.to/rolandstuder/supercharged-table-component-built-with-viewcomponent-3j6i]
|
|
22
|
+
def before_render
|
|
23
|
+
content
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Column
|
|
28
|
+
attr_reader :label, :cell
|
|
29
|
+
|
|
30
|
+
def initialize(label, &block)
|
|
31
|
+
@label = label
|
|
32
|
+
@cell = block
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class TagColumn < Column
|
|
37
|
+
attr_reader :label, :cell
|
|
38
|
+
|
|
39
|
+
def initialize(label, &block)
|
|
40
|
+
@label = label
|
|
41
|
+
@cell = Panda::CMS::Admin::TagComponent.new(status: block)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
module Admin
|
|
6
|
+
class TagComponent < ViewComponent::Base
|
|
7
|
+
attr_accessor :status, :text
|
|
8
|
+
|
|
9
|
+
def initialize(status: :active, text: nil)
|
|
10
|
+
@status = status.to_sym
|
|
11
|
+
@text = text || status.to_s.humanize
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
classes = "inline-flex items-center py-1 px-2 text-xs font-medium rounded-md ring-1 ring-inset "
|
|
16
|
+
|
|
17
|
+
classes += case @status
|
|
18
|
+
when :active
|
|
19
|
+
"text-white ring-black/30 bg-active border-0 "
|
|
20
|
+
when :draft
|
|
21
|
+
"text-black ring-black/30 bg-warning "
|
|
22
|
+
when :inactive, :hidden
|
|
23
|
+
"text-black ring-black/30 bg-black/5 bg-white "
|
|
24
|
+
else
|
|
25
|
+
"text-black bg-white "
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
content_tag :span, class: classes do
|
|
29
|
+
@text
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
module Admin
|
|
6
|
+
class UserActivityComponent < ViewComponent::Base
|
|
7
|
+
attr_accessor :model
|
|
8
|
+
attr_accessor :time
|
|
9
|
+
attr_accessor :user
|
|
10
|
+
|
|
11
|
+
# @param whodunnit_to [ActiveRecord::Base] Model instance to which the user activity is related
|
|
12
|
+
def initialize(whodunnit_to: nil, at: nil, user: nil)
|
|
13
|
+
if whodunnit_to
|
|
14
|
+
@model = whodunnit_to
|
|
15
|
+
whodunnit_id = @model.versions&.last&.whodunnit
|
|
16
|
+
if whodunnit_id
|
|
17
|
+
@user = User.find(whodunnit_id)
|
|
18
|
+
@time = @model.updated_at
|
|
19
|
+
end
|
|
20
|
+
elsif user.is_a?(::Panda::CMS::User) && at.is_a?(::ActiveSupport::TimeWithZone)
|
|
21
|
+
@user = user
|
|
22
|
+
@time = at
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
if !@time
|
|
26
|
+
@user = nil
|
|
27
|
+
@time = nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<div class="block flex-shrink-0 group">
|
|
2
|
+
<div class="flex items-center">
|
|
3
|
+
<% unless user.image_url.empty? %>
|
|
4
|
+
<div>
|
|
5
|
+
<img class="inline-block w-10 h-10 rounded-full" src="<%= user.image_url %>" alt="">
|
|
6
|
+
</div>
|
|
7
|
+
<% else %>
|
|
8
|
+
<div class="inline-block w-10 h-10 bg-gray-200 rounded-full">
|
|
9
|
+
<span class="text-center"><i class="text-mid/50 text-4xl fa-sharp fa-circle-user fa-fw"></i></span>
|
|
10
|
+
</div>
|
|
11
|
+
<% end %>
|
|
12
|
+
<div class="ml-3">
|
|
13
|
+
<p class="text-sm text-black"><%= user.firstname %> <%= user.lastname %></p>
|
|
14
|
+
<% if metadata %><p class="text-sm text-black/60"><%= metadata %></p><% end %>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
module Admin
|
|
6
|
+
class UserDisplayComponent < ViewComponent::Base
|
|
7
|
+
attr_accessor :user_id, :user, :metadata
|
|
8
|
+
|
|
9
|
+
def initialize(user_id: nil, user: nil, metadata: "")
|
|
10
|
+
@user = if user.nil? && user_id.present? && Panda::CMS::User.find(user_id)
|
|
11
|
+
Panda::CMS::User.find(user_id)
|
|
12
|
+
else
|
|
13
|
+
user
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
@metadata = metadata
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
# Text component
|
|
6
|
+
# @param key [Symbol] The key to use for the text component
|
|
7
|
+
# @param text [String] The text to display
|
|
8
|
+
# @param editable [Boolean] If the text is editable or not (defaults to true)
|
|
9
|
+
# @param options [Hash] The options to pass to the content_tag
|
|
10
|
+
class CodeComponent < ViewComponent::Base
|
|
11
|
+
KIND = "code"
|
|
12
|
+
|
|
13
|
+
def initialize(key: :text_component, text: "", editable: true, **options)
|
|
14
|
+
@key = key
|
|
15
|
+
@text = text
|
|
16
|
+
@options = options || {}
|
|
17
|
+
@options[:id] ||= "code-#{key.to_s.dasherize}"
|
|
18
|
+
@editable = editable
|
|
19
|
+
|
|
20
|
+
raise BlockError.new("Key 'code' is not allowed for CodeComponent") if key == :code
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call
|
|
24
|
+
# TODO: For the non-editable version, grab this from a cache or similar?
|
|
25
|
+
block = Panda::CMS::Block.find_by(kind: KIND, key: @key, panda_cms_template_id: Current.page.panda_cms_template_id)
|
|
26
|
+
|
|
27
|
+
if block.nil?
|
|
28
|
+
raise Panda::CMS::MissingBlockError.new("Block with key #{@key} not found for page #{Current.page.title}") unless Rails.env.production?
|
|
29
|
+
return false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
block_content = block.block_contents.find_by(panda_cms_page_id: Current.page.id)
|
|
33
|
+
code_content = block_content&.content.to_s
|
|
34
|
+
|
|
35
|
+
if component_is_editable?
|
|
36
|
+
@options[:contenteditable] = "plaintext-only"
|
|
37
|
+
@options[:data] = {
|
|
38
|
+
"editable-kind": "html",
|
|
39
|
+
"editable-page-id": Current.page.id,
|
|
40
|
+
"editable-block-content-id": block_content&.id
|
|
41
|
+
}
|
|
42
|
+
@options[:class] = "block bg-yellow-50 font-mono p-2 border-2 border-yellow-700"
|
|
43
|
+
@options[:style] = "white-space: pre-wrap;"
|
|
44
|
+
|
|
45
|
+
@options[:id] = "editor-#{block_content&.id}"
|
|
46
|
+
# TODO: Switch between the HTML and the preview?
|
|
47
|
+
content_tag(:div, code_content, @options, true)
|
|
48
|
+
else
|
|
49
|
+
code_content.html_safe
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def component_is_editable?
|
|
54
|
+
# TODO: Permissions
|
|
55
|
+
@editable && is_embedded? && Current.user&.admin
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def is_embedded?
|
|
59
|
+
# TODO: Check security on this - embed_id should match something?
|
|
60
|
+
request.params.dig(:embed_id).present?
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
class GridComponent < ViewComponent::Base
|
|
6
|
+
def initialize(columns: 1, spans: [1])
|
|
7
|
+
@columns = "grid-cols-#{columns}"
|
|
8
|
+
@colspans = []
|
|
9
|
+
spans.each do |span|
|
|
10
|
+
@colspans << "col-span-#{span}"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<% @menu_items.each do |menu_item| %>
|
|
2
|
+
<a href="<%= menu_item.resolved_link %>" class="<%= menu_item.css_classes %>"><%= menu_item.text %></a>
|
|
3
|
+
<% if @render_page_menu && menu_item.page %>
|
|
4
|
+
<%= render Panda::CMS::PageMenuComponent.new(page: menu_item.page, start_depth: 1, styles: @page_menu_styles, show_heading: false) %>
|
|
5
|
+
<% end %>
|
|
6
|
+
<% end %>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
class MenuComponent < ViewComponent::Base
|
|
6
|
+
#
|
|
7
|
+
# Renders the menu item and its children
|
|
8
|
+
#
|
|
9
|
+
# @param [String] name The name of the menu
|
|
10
|
+
# @param [String] current_path The current path of the request (request.path)
|
|
11
|
+
# @param [Hash] styles
|
|
12
|
+
# The CSS classes to apply to the menu items, containing "default", "inactive" and "active" keys.
|
|
13
|
+
# The "default" key is applied to all menu items. "inactive" and "active" are set based on the
|
|
14
|
+
# current path.
|
|
15
|
+
# @return [void]
|
|
16
|
+
def initialize(name:, current_path: "", styles: {}, overrides: {}, render_page_menu: false, page_menu_styles: {})
|
|
17
|
+
@menu = Panda::CMS::Menu.find_by(name: name)
|
|
18
|
+
@menu_items = @menu.menu_items
|
|
19
|
+
@menu_items = @menu_items.where("depth <= ?", @menu.depth) if @menu.depth
|
|
20
|
+
@menu_items = @menu_items.order(:lft)
|
|
21
|
+
@current_path = current_path.to_s
|
|
22
|
+
@render_page_menu = render_page_menu
|
|
23
|
+
|
|
24
|
+
@menu_items = @menu_items.order(:lft).map do |menu_item|
|
|
25
|
+
if is_active?(menu_item)
|
|
26
|
+
menu_item.define_singleton_method(:css_classes) { styles[:default] + " " + styles[:active] }
|
|
27
|
+
else
|
|
28
|
+
menu_item.define_singleton_method(:css_classes) { styles[:default] + " " + styles[:inactive] }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
menu_item
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# TODO: Surely don't need this but Current.page isn't working in the component
|
|
35
|
+
if @render_page_menu
|
|
36
|
+
@current_page = Panda::CMS::Page.find_by(path: @current_path)
|
|
37
|
+
@page_menu_styles = page_menu_styles
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def is_active?(menu_item)
|
|
42
|
+
return true if @current_path == "/" && active_link?(menu_item.page.path, match: :exact)
|
|
43
|
+
return true if menu_item.page.path != "/" && active_link?(menu_item.page.path, match: :starts_with)
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def active_link?(path, match: :starts_with)
|
|
48
|
+
if match == :starts_with
|
|
49
|
+
return @current_path.starts_with?(path)
|
|
50
|
+
elsif match == :exact
|
|
51
|
+
return (@current_path == path)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<nav class="<%= styles[:container] %>">
|
|
2
|
+
<ul role="list" class="p-0 m-0">
|
|
3
|
+
<% if @show_heading %>
|
|
4
|
+
<li>
|
|
5
|
+
<a href="<%= menu_item.page.path %>" class="<%= menu_item.page == Panda::CMS::Current.page ? styles[:current_page_active] : styles[:current_page_inactive] %>">
|
|
6
|
+
<%= menu_item.text %>
|
|
7
|
+
</a>
|
|
8
|
+
</li>
|
|
9
|
+
<% end %>
|
|
10
|
+
<ul>
|
|
11
|
+
<% Panda::CMS::MenuItem.includes(:page).each_with_level(menu_item.descendants) do |submenu_item, level| %>
|
|
12
|
+
<% next if Panda::CMS::Current.page == menu_item.page && level > 1 # If we're on the "top" menu item, only show its direct ancestors %>
|
|
13
|
+
<% next if submenu_item.page&.path[/\:/] %>
|
|
14
|
+
<% next if submenu_item&.page.nil? || Panda::CMS::Current.page.nil? || (submenu_item.page&.depth&.to_i > Panda::CMS::Current.page&.depth&.to_i && !Panda::CMS::Current.page&.in?(submenu_item.page.ancestors)) %>
|
|
15
|
+
<li data-level="<%= level %>" data-page-id="<%= submenu_item.page.id %>" class="<%= submenu_item.page == Panda::CMS::Current.page ? @styles[:active] : @styles[:inactive] %>">
|
|
16
|
+
<a href="<%= submenu_item.page&.path %>" class="<%= helpers.menu_indent(submenu_item, indent_with: @styles[:indent_with]) %>"><%= submenu_item.page&.title %></a>
|
|
17
|
+
</li>
|
|
18
|
+
<% end %>
|
|
19
|
+
</ul>
|
|
20
|
+
</ul>
|
|
21
|
+
</nav>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module CMS
|
|
5
|
+
class PageMenuComponent < ViewComponent::Base
|
|
6
|
+
attr_accessor :page
|
|
7
|
+
attr_accessor :menu_item
|
|
8
|
+
attr_accessor :styles
|
|
9
|
+
|
|
10
|
+
def initialize(page:, start_depth:, styles: {}, show_heading: true)
|
|
11
|
+
@page = page
|
|
12
|
+
|
|
13
|
+
unless @page.nil?
|
|
14
|
+
start_page = if @page.depth == start_depth
|
|
15
|
+
@page
|
|
16
|
+
else
|
|
17
|
+
@page.ancestors.find { |anc| anc.depth == start_depth }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
menu = start_page&.page_menu
|
|
21
|
+
return if menu.nil?
|
|
22
|
+
|
|
23
|
+
@menu_item = menu.menu_items.order(:lft)&.first
|
|
24
|
+
|
|
25
|
+
@show_heading = show_heading
|
|
26
|
+
|
|
27
|
+
# Set some default styles for sanity
|
|
28
|
+
@styles = styles
|
|
29
|
+
@styles[:indent_with] ||= "pl-2"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def render?
|
|
34
|
+
@page&.path != "/" && @menu_item.present?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<% if @editable %>
|
|
2
|
+
<div class="panda-cms-content" data-editable-previous-data="<%= @content.to_json %>" id="editor-<%= @options[:id] %>" data-editable-kind="rich_text" data-editable-block-content-id="<%= @options[:id] %>" data-editable-page-id="<%= @options[:data][:page_id] %>" style="border: 1px dashed #ccc; outline: none; cursor: pointer; transition: background 500ms linear; background-color: inherit;">
|
|
3
|
+
</div>
|
|
4
|
+
<% else %>
|
|
5
|
+
<div class="panda-cms-content"><%= @content %></div>
|
|
6
|
+
<% end %>
|