mirror of
https://github.com/indentlabs/notebook.git
synced 2025-10-26 11:19:22 +00:00
Consolidate attributes editor and fix dashboard image errors
- Merged duplicate attributes editors into single route - Removed /plan/:content_type/attributes-tailwind route and controller method - Moved modern Tailwind attributes editor to main /attributes route - Fixed dashboard ArgumentError: Nil location provided. Can't build URI - Enhanced HasImageUploads concern with better Active Storage handling - Added nil-safety guards to header_asset_for method - Improved caching logic to prevent nil value storage 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a381a4a3f3
commit
ada4b03f92
@ -3,7 +3,7 @@
|
||||
# TODO: we should probably spin off an Api::ContentController for #api_sort and anything else
|
||||
# api-wise we need
|
||||
class ContentController < ApplicationController
|
||||
layout 'tailwind', only: [:index, :show, :gallery, :references, :deleted, :attributes_tailwind]
|
||||
layout 'tailwind', only: [:index, :show, :gallery, :references, :deleted, :attributes]
|
||||
|
||||
before_action :authenticate_user!, except: [:show, :changelog, :api_sort, :gallery] \
|
||||
+ Rails.application.config.content_types[:all_non_universe].map { |type| type.name.downcase.pluralize.to_sym }
|
||||
@ -16,7 +16,7 @@ class ContentController < ApplicationController
|
||||
|
||||
before_action :cache_linkable_content_for_each_content_type, only: [:new, :show, :edit, :index]
|
||||
|
||||
before_action :set_attributes_content_type, only: [:attributes, :attributes_tailwind, :export_template, :reset_template]
|
||||
before_action :set_attributes_content_type, only: [:attributes, :export_template, :reset_template]
|
||||
|
||||
before_action :set_navbar_color, except: [:api_sort]
|
||||
before_action :set_navbar_actions, except: [:deleted, :api_sort]
|
||||
@ -500,18 +500,6 @@ class ContentController < ApplicationController
|
||||
@dummy_model = @content_type_class.new
|
||||
end
|
||||
|
||||
def attributes_tailwind
|
||||
@attribute_categories = @content_type_class
|
||||
.attribute_categories(current_user, show_hidden: true)
|
||||
.shown_on_template_editor
|
||||
.order(:position)
|
||||
|
||||
@dummy_model = @content_type_class.new
|
||||
|
||||
# Use the Tailwind layout
|
||||
render :attributes_tailwind
|
||||
end
|
||||
|
||||
def export_template
|
||||
service = TemplateExportService.new(current_user, @content_type)
|
||||
|
||||
|
||||
@ -35,17 +35,23 @@ module HasImageUploads
|
||||
|
||||
# If we don't have any uploaded images, we look for saved Basil commissions
|
||||
if result.nil? && respond_to?(:basil_commissions)
|
||||
result = basil_commissions.where.not(saved_at: nil).includes([:image_attachment]).sample.try(:image)
|
||||
basil_image = basil_commissions.where.not(saved_at: nil).includes([:image_attachment]).sample.try(:image)
|
||||
# Handle Active Storage attachments properly
|
||||
if basil_image.present? && basil_image.respond_to?(:url)
|
||||
begin
|
||||
result = basil_image.url
|
||||
rescue
|
||||
result = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Cache the result
|
||||
@random_image_including_private_cache[key] = result
|
||||
# Cache the result (only cache non-nil results to avoid issues)
|
||||
@random_image_including_private_cache[key] = result if result.present?
|
||||
|
||||
# Finally, if we have no image upload, we return the default image for this type
|
||||
result = result.presence || header_asset_for(self.class.name)
|
||||
|
||||
result
|
||||
# Finally, if we have no valid image URL, return the default image for this type
|
||||
result.presence || header_asset_for(self.class.name)
|
||||
end
|
||||
|
||||
def first_public_image(format = :medium)
|
||||
@ -75,7 +81,17 @@ module HasImageUploads
|
||||
# Then check basil commissions
|
||||
if respond_to?(:basil_commissions)
|
||||
pinned_commission = basil_commissions.pinned.where.not(saved_at: nil).includes([:image_attachment]).first
|
||||
return pinned_commission.try(:image) if pinned_commission.present?
|
||||
if pinned_commission.present?
|
||||
basil_image = pinned_commission.try(:image)
|
||||
# Handle Active Storage attachments properly
|
||||
if basil_image.present? && basil_image.respond_to?(:url)
|
||||
begin
|
||||
return basil_image.url
|
||||
rescue
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
nil
|
||||
@ -88,7 +104,17 @@ module HasImageUploads
|
||||
|
||||
if respond_to?(:basil_commissions)
|
||||
pinned_commission = basil_commissions.pinned.where.not(saved_at: nil).includes([:image_attachment]).first
|
||||
return pinned_commission.try(:image) if pinned_commission.present?
|
||||
if pinned_commission.present?
|
||||
basil_image = pinned_commission.try(:image)
|
||||
# Handle Active Storage attachments properly
|
||||
if basil_image.present? && basil_image.respond_to?(:url)
|
||||
begin
|
||||
return basil_image.url
|
||||
rescue
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
nil
|
||||
@ -109,9 +135,14 @@ module HasImageUploads
|
||||
# will not work.
|
||||
#
|
||||
# For direct view rendering, we use the relative asset path which works better with image_tag
|
||||
Rails.env.production? ?
|
||||
"https://www.notebook.ai" + ActionController::Base.helpers.asset_url("card-headers/#{class_name.downcase.pluralize}.webp") :
|
||||
ActionController::Base.helpers.asset_path("card-headers/#{class_name.downcase.pluralize}.webp")
|
||||
asset_filename = "card-headers/#{class_name.downcase.pluralize}.webp"
|
||||
|
||||
result = Rails.env.production? ?
|
||||
"https://www.notebook.ai" + ActionController::Base.helpers.asset_url(asset_filename) :
|
||||
ActionController::Base.helpers.asset_path(asset_filename)
|
||||
|
||||
# Ensure we never return nil - provide a fallback
|
||||
result.presence || (Rails.env.production? ? "https://www.notebook.ai/assets/#{asset_filename}" : "/assets/#{asset_filename}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -2,117 +2,345 @@
|
||||
<%= render partial: 'content/components/parallax_header', locals: { content_type: @content_type, content_class: @content_type_class } %>
|
||||
<% end %>
|
||||
|
||||
<!--
|
||||
<div class="row">
|
||||
<div class="col s12 m4">
|
||||
<%= link_to '#' do %>
|
||||
<div class="hoverable card">
|
||||
<div class="card-content">
|
||||
<div class="card-title">
|
||||
<i class="material-icons left"><%= @content_type_class.icon %></i>
|
||||
Customize icon
|
||||
</div>
|
||||
You can customize this page.
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= javascript_pack_tag 'template_editor' %>
|
||||
|
||||
|
||||
<div class="attributes-editor max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"
|
||||
data-content-type="<%= @content_type %>"
|
||||
style="--content-type-color: <%= @content_type_class.hex_color %>;"
|
||||
x-data="initTemplateEditor()">
|
||||
|
||||
<!-- Mobile Panel Switch Tabs (visible only on small screens) -->
|
||||
<div class="md:hidden bg-white border-b border-gray-200 sticky top-0 z-10">
|
||||
<div class="flex -mb-px">
|
||||
<button @click="activePanel = 'template'"
|
||||
:style="activePanel === 'template' ? 'border-color: var(--content-type-color); color: var(--content-type-color)' : ''"
|
||||
:class="activePanel === 'template' ? '' : 'border-transparent text-gray-500'"
|
||||
class="flex-1 py-4 px-1 text-center border-b-2 font-medium text-sm transition-colors">
|
||||
Template Editor
|
||||
</button>
|
||||
<button @click="activePanel = 'config'; configuring = true;"
|
||||
:style="activePanel === 'config' ? 'border-color: var(--content-type-color); color: var(--content-type-color)' : ''"
|
||||
:class="activePanel === 'config' ? '' : 'border-transparent text-gray-500'"
|
||||
class="flex-1 py-4 px-1 text-center border-b-2 font-medium text-sm transition-colors">
|
||||
Configuration
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col s12 m4">
|
||||
<%= link_to '#' do %>
|
||||
<div class="hoverable card white-text" style="background-color: <%= @content_type_class.hex_color_for(current_user) %>">
|
||||
<div class="card-content">
|
||||
<div class="card-title">Customize color</div>
|
||||
You can recolor this page.
|
||||
|
||||
<!-- Main Content Area - Two Panel Layout -->
|
||||
<div class="flex flex-col md:flex-row gap-6 mt-4">
|
||||
<!-- Template Panel (always visible on desktop, conditionally on mobile) -->
|
||||
<div :class="{ 'hidden md:block': activePanel === 'config' }"
|
||||
class="flex-1 min-w-0">
|
||||
|
||||
<!-- Streamlined Header -->
|
||||
<div class="template-header relative overflow-hidden rounded-lg shadow-md mb-6 bg-white">
|
||||
<!-- Background with Frosted Glass Effect -->
|
||||
<div class="absolute inset-0">
|
||||
<%= image_tag "card-headers/#{@content_type.downcase.pluralize}.jpg",
|
||||
class: "hero-bg-image w-full h-full object-cover",
|
||||
alt: "#{@content_type.titleize} background" %>
|
||||
<!-- Frosted glass overlay for better text readability -->
|
||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-lg"></div>
|
||||
<div class="absolute inset-0" style="background: linear-gradient(135deg, <%= @content_type_class.hex_color %>aa 0%, <%= @content_type_class.hex_color %>66 50%, rgba(0,0,0,0.3) 100%);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="col s12 m4">
|
||||
<%= link_to '#' do %>
|
||||
<div class="hoverable card">
|
||||
<div class="card-content">
|
||||
<div class="card-title">Customize header image</div>
|
||||
You can change the header image.
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div class="my-4 text-center">
|
||||
<%= link_to attribute_customization_tailwind_path(content_type: @content_type), class: 'btn purple' do %>
|
||||
<i class="material-icons left">star</i>Try the new template editor
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="attributes-editor" data-content-type="<%= @content_type %>">
|
||||
<ul class="hoverable sortable collapsible white" data-sortable-class="AttributeCategory">
|
||||
<%= render partial: 'content/attributes/general_settings', locals: { content_type: @content_type, content_type_class: @content_type_class } %>
|
||||
|
||||
<% @attribute_categories.each do |attribute_category| %>
|
||||
<li data-position="<%= attribute_category.position %>" data-content-id="<%= attribute_category.id %>" class="js-category-container">
|
||||
<div class="collapsible-header">
|
||||
<span class="sortable-handle"><i class="material-icons grey-text">menu</i></span>
|
||||
<i class="material-icons"><%= attribute_category.icon %></i>
|
||||
<div class="js-category-label">
|
||||
<%= attribute_category.label %>
|
||||
</div>
|
||||
<% if attribute_category.hidden? %>
|
||||
<span class="grey-text">—</span>
|
||||
<i class="material-icons grey-text tooltipped" data-tooltip="You've hidden this category.">visibility_off</i>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="collapsible-body">
|
||||
<div class="row">
|
||||
<% if attribute_category.hidden? %>
|
||||
<div class="col s12 m12 l12">
|
||||
<%= render partial: 'content/attributes/category_visibility_controls', locals: { category: attribute_category } %>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="relative z-10 px-6 py-5">
|
||||
<div class="flex items-start justify-between">
|
||||
<!-- Icon and Title -->
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 rounded-lg flex items-center justify-center mr-4 content-type-icon-bg">
|
||||
<i class="material-icons text-white text-xl"><%= @content_type_class.icon %></i>
|
||||
</div>
|
||||
|
||||
<% else %>
|
||||
<div class="col s12 m12 l8">
|
||||
<%= render partial: 'content/attributes/category_fields_customization', locals: { category: attribute_category } %>
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-white leading-tight mb-1">
|
||||
<%= @content_type.titleize %> Template Editor
|
||||
</h1>
|
||||
<p class="text-white text-sm leading-relaxed max-w-2xl">
|
||||
Edit the template used for your <%= @content_type.titleize.downcase %> pages. Adding or removing a field here will modify all of your already-created <%= @content_type.titleize.downcase %> pages also.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col s12 m12 l4">
|
||||
<%= render partial: 'content/attributes/category_ui_customization', locals: { category: attribute_category } %>
|
||||
<%= render partial: 'content/attributes/category_visibility_controls', locals: { category: attribute_category } %>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
<li>
|
||||
<div class="collapsible-header">
|
||||
<i class="material-icons <%= @content_type_class.text_color %>">add</i>
|
||||
Add another category
|
||||
</div>
|
||||
<div class="collapsible-body">
|
||||
<%= form_for(current_user.attribute_categories.build, method: :post) do |f| %>
|
||||
<%= f.hidden_field :entity_type, value: @content_type %>
|
||||
<div class="row">
|
||||
<div class="input-field col s12">
|
||||
<%= f.text_area :label, class: 'materialize-textarea js-category-input' %>
|
||||
<%= f.label :label, 'Category label' %>
|
||||
</div>
|
||||
<div class="input-field col s12">
|
||||
<%= f.submit 'Add new category', class: "btn #{@content_type_class.color}" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="grey-text text-darken-1">
|
||||
<strong>New:</strong> Notebook.ai can now suggest additional categories for your pages.
|
||||
</div>
|
||||
<p class="suggest-categories-container">
|
||||
</p>
|
||||
<%= f.button "Show suggestions", class: 'small btn white black-text js-show-category-suggestions' %>
|
||||
</div>
|
||||
<!-- Categories Container -->
|
||||
<div id="categories-container" class="space-y-4" data-sortable-class="AttributeCategory">
|
||||
<% @attribute_categories.each do |category| %>
|
||||
<%= render partial: 'content/attributes/tailwind/category_card', locals: {
|
||||
category: category,
|
||||
content_type_class: @content_type_class,
|
||||
content_type: @content_type
|
||||
} %>
|
||||
<% end %>
|
||||
|
||||
<!-- Add Category Card -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-dashed border-gray-300 hover:border-gray-400 transition-colors p-4">
|
||||
<div class="flex justify-center items-center p-6 cursor-pointer"
|
||||
data-action="click->attributes-editor#showAddCategoryForm">
|
||||
<i class="material-icons text-gray-400 mr-2">add_circle_outline</i>
|
||||
<span class="text-gray-500 font-medium">Add a New Category</span>
|
||||
</div>
|
||||
|
||||
<!-- Category Form (Hidden by Default) -->
|
||||
<div id="add-category-form" class="hidden p-4 border-t border-gray-200 mt-4">
|
||||
<%= form_for(current_user.attribute_categories.build, method: :post, html: { class: "space-y-4", 'data-type': 'json', 'data-remote': 'true', '@submit.prevent': 'submitCategoryForm($event)' }, remote: true) do |f| %>
|
||||
<%= f.hidden_field :entity_type, value: @content_type %>
|
||||
|
||||
<div>
|
||||
<label for="category_label" class="block text-sm font-medium text-gray-700 mb-1">Category Name</label>
|
||||
<%= f.text_field :label, class: "shadow-sm block w-full sm:text-sm border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2", style: "--tw-ring-color: var(--content-type-color); --tw-border-opacity: 1; border-color: var(--content-type-color);" %>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button"
|
||||
@click="document.getElementById('add-category-form').classList.add('hidden')"
|
||||
class="px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none">
|
||||
Cancel
|
||||
</button>
|
||||
<%= f.submit "Add Category", class: "px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white focus:outline-none focus:ring-2 focus:ring-offset-2", style: "background-color: var(--content-type-color); --tw-ring-color: var(--content-type-color);" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-6 pt-4 border-t border-gray-200">
|
||||
<div class="text-sm font-medium text-gray-700 mb-2">Category Suggestions</div>
|
||||
<p class="text-sm text-gray-500 mb-2">
|
||||
Notebook.ai can suggest additional categories for your pages.
|
||||
</p>
|
||||
<div class="suggest-categories-container flex flex-wrap gap-2 mb-3"></div>
|
||||
<button type="button" class="js-show-category-suggestions px-3 py-1.5 border border-gray-300 rounded-md text-xs font-medium text-gray-700 hover:bg-gray-50 focus:outline-none">
|
||||
Show Suggestions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Panel (always visible on desktop, conditionally on mobile) -->
|
||||
<div :class="{ 'hidden md:block': activePanel === 'template' && !configuring }"
|
||||
class="w-full md:w-96 lg:w-96"
|
||||
id="config-panel-container">
|
||||
|
||||
<!-- Sticky Container for Desktop/Tablet -->
|
||||
<div class="config-panel-sticky">
|
||||
|
||||
<!-- No Selection State -->
|
||||
<div x-show="!selectedCategory && !selectedField && !configuring"
|
||||
class="bg-white rounded-lg shadow-md p-6 text-center config-panel-content">
|
||||
<div class="mx-auto w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4">
|
||||
<i class="material-icons text-gray-400 text-2xl">tune</i>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Configure Your Template</h3>
|
||||
<p class="text-sm text-gray-500 mb-4">
|
||||
Select a category or field to customize it, or add a new one to get started.
|
||||
</p>
|
||||
<p class="text-xs text-gray-400 mb-1">Tip: You can drag and drop to reorder categories and fields.</p>
|
||||
<button @click="configuring = true" class="mt-2 px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none">
|
||||
<span>General Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Field Configuration -->
|
||||
<div x-show="selectedField"
|
||||
class="bg-white rounded-lg shadow-md overflow-hidden config-panel-content">
|
||||
<!-- Content will be loaded dynamically -->
|
||||
<div id="field-config-container" class="p-4">
|
||||
<div class="flex justify-center items-center p-8">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2" style="border-color: var(--content-type-color);"></div>
|
||||
<span class="ml-2 text-gray-600">Loading field settings...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Configuration -->
|
||||
<div x-show="selectedCategory"
|
||||
class="bg-white rounded-lg shadow-md overflow-hidden config-panel-content">
|
||||
<!-- Content will be loaded dynamically -->
|
||||
<div id="category-config-container" class="p-4">
|
||||
<div class="flex justify-center items-center p-8">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2" style="border-color: var(--content-type-color);"></div>
|
||||
<span class="ml-2 text-gray-600">Loading category settings...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- General Configuration -->
|
||||
<div x-show="configuring && !selectedCategory && !selectedField"
|
||||
class="bg-white rounded-lg shadow-md overflow-hidden config-panel-content">
|
||||
<%= render partial: 'content/attributes/tailwind/general_settings', locals: {
|
||||
content_type: @content_type,
|
||||
content_type_class: @content_type_class
|
||||
} %>
|
||||
</div>
|
||||
|
||||
</div> <!-- End sticky container -->
|
||||
</div> <!-- End config panel container -->
|
||||
</div> <!-- End main content area -->
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
/* Streamlined Header Styles */
|
||||
.template-header {
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.template-header:hover {
|
||||
shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* Subtle background image effect */
|
||||
.hero-bg-image {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.template-header:hover .hero-bg-image {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Content type icon styling */
|
||||
.content-type-icon-bg {
|
||||
background: linear-gradient(135deg, var(--content-type-color) 0%, color-mix(in srgb, var(--content-type-color) 80%, white) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.content-type-icon-bg:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Interactive hint styling */
|
||||
.interactive-hint {
|
||||
transition: all 0.2s ease;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.interactive-hint:hover {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.interactive-hint:hover i {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* jQuery UI Sortable placeholder styles */
|
||||
.category-placeholder {
|
||||
visibility: visible !important;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 2px dashed rgba(59, 130, 246, 0.3);
|
||||
border-radius: 8px;
|
||||
height: 120px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.field-placeholder {
|
||||
visibility: visible !important;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 2px dashed rgba(59, 130, 246, 0.3);
|
||||
border-radius: 6px;
|
||||
height: 60px;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Improve drag handle appearance */
|
||||
.category-drag-handle,
|
||||
.field-drag-handle {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.category-drag-handle:active,
|
||||
.field-drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* Category header styling */
|
||||
.category-header {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.category-header:hover {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.category-header:active {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
/* Sortable helper styles */
|
||||
.ui-sortable-helper {
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15) !important;
|
||||
transform: rotate(2deg);
|
||||
}
|
||||
|
||||
/* Notification animations */
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-notification-in {
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Loading spinner animation */
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Smart sticky configuration panel */
|
||||
.config-panel-sticky {
|
||||
position: sticky;
|
||||
top: 2rem;
|
||||
transition: all 0.2s ease-out;
|
||||
max-height: calc(100vh - 4rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* On mobile, don't make it sticky to avoid layout issues */
|
||||
@media (max-width: 768px) {
|
||||
.config-panel-sticky {
|
||||
position: static;
|
||||
max-height: none;
|
||||
overflow-y: visible;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure the configuration panels have proper scrolling */
|
||||
.config-panel-content {
|
||||
max-height: calc(100vh - 6rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.config-panel-content {
|
||||
max-height: none;
|
||||
overflow-y: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Template editor JavaScript functionality is loaded via the template_editor pack -->
|
||||
@ -1,346 +0,0 @@
|
||||
<%= content_for :full_width_page_header do %>
|
||||
<%= render partial: 'content/components/parallax_header', locals: { content_type: @content_type, content_class: @content_type_class } %>
|
||||
<% end %>
|
||||
|
||||
<%= javascript_pack_tag 'template_editor' %>
|
||||
|
||||
|
||||
<div class="attributes-editor max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"
|
||||
data-content-type="<%= @content_type %>"
|
||||
style="--content-type-color: <%= @content_type_class.hex_color %>;"
|
||||
x-data="initTemplateEditor()">
|
||||
|
||||
<!-- Mobile Panel Switch Tabs (visible only on small screens) -->
|
||||
<div class="md:hidden bg-white border-b border-gray-200 sticky top-0 z-10">
|
||||
<div class="flex -mb-px">
|
||||
<button @click="activePanel = 'template'"
|
||||
:style="activePanel === 'template' ? 'border-color: var(--content-type-color); color: var(--content-type-color)' : ''"
|
||||
:class="activePanel === 'template' ? '' : 'border-transparent text-gray-500'"
|
||||
class="flex-1 py-4 px-1 text-center border-b-2 font-medium text-sm transition-colors">
|
||||
Template Editor
|
||||
</button>
|
||||
<button @click="activePanel = 'config'; configuring = true;"
|
||||
:style="activePanel === 'config' ? 'border-color: var(--content-type-color); color: var(--content-type-color)' : ''"
|
||||
:class="activePanel === 'config' ? '' : 'border-transparent text-gray-500'"
|
||||
class="flex-1 py-4 px-1 text-center border-b-2 font-medium text-sm transition-colors">
|
||||
Configuration
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Area - Two Panel Layout -->
|
||||
<div class="flex flex-col md:flex-row gap-6 mt-4">
|
||||
<!-- Template Panel (always visible on desktop, conditionally on mobile) -->
|
||||
<div :class="{ 'hidden md:block': activePanel === 'config' }"
|
||||
class="flex-1 min-w-0">
|
||||
|
||||
<!-- Streamlined Header -->
|
||||
<div class="template-header relative overflow-hidden rounded-lg shadow-md mb-6 bg-white">
|
||||
<!-- Background with Frosted Glass Effect -->
|
||||
<div class="absolute inset-0">
|
||||
<%= image_tag "card-headers/#{@content_type.downcase.pluralize}.jpg",
|
||||
class: "hero-bg-image w-full h-full object-cover",
|
||||
alt: "#{@content_type.titleize} background" %>
|
||||
<!-- Frosted glass overlay for better text readability -->
|
||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-lg"></div>
|
||||
<div class="absolute inset-0" style="background: linear-gradient(135deg, <%= @content_type_class.hex_color %>aa 0%, <%= @content_type_class.hex_color %>66 50%, rgba(0,0,0,0.3) 100%);"></div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="relative z-10 px-6 py-5">
|
||||
<div class="flex items-start justify-between">
|
||||
<!-- Icon and Title -->
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 rounded-lg flex items-center justify-center mr-4 content-type-icon-bg">
|
||||
<i class="material-icons text-white text-xl"><%= @content_type_class.icon %></i>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-white leading-tight mb-1">
|
||||
<%= @content_type.titleize %> Template Editor
|
||||
</h1>
|
||||
<p class="text-white text-sm leading-relaxed max-w-2xl">
|
||||
Edit the template used for your <%= @content_type.titleize.downcase %> pages. Adding or removing a field here will modify all of your already-created <%= @content_type.titleize.downcase %> pages also.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Categories Container -->
|
||||
<div id="categories-container" class="space-y-4" data-sortable-class="AttributeCategory">
|
||||
<% @attribute_categories.each do |category| %>
|
||||
<%= render partial: 'content/attributes/tailwind/category_card', locals: {
|
||||
category: category,
|
||||
content_type_class: @content_type_class,
|
||||
content_type: @content_type
|
||||
} %>
|
||||
<% end %>
|
||||
|
||||
<!-- Add Category Card -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-dashed border-gray-300 hover:border-gray-400 transition-colors p-4">
|
||||
<div class="flex justify-center items-center p-6 cursor-pointer"
|
||||
data-action="click->attributes-editor#showAddCategoryForm">
|
||||
<i class="material-icons text-gray-400 mr-2">add_circle_outline</i>
|
||||
<span class="text-gray-500 font-medium">Add a New Category</span>
|
||||
</div>
|
||||
|
||||
<!-- Category Form (Hidden by Default) -->
|
||||
<div id="add-category-form" class="hidden p-4 border-t border-gray-200 mt-4">
|
||||
<%= form_for(current_user.attribute_categories.build, method: :post, html: { class: "space-y-4", 'data-type': 'json', 'data-remote': 'true', '@submit.prevent': 'submitCategoryForm($event)' }, remote: true) do |f| %>
|
||||
<%= f.hidden_field :entity_type, value: @content_type %>
|
||||
|
||||
<div>
|
||||
<label for="category_label" class="block text-sm font-medium text-gray-700 mb-1">Category Name</label>
|
||||
<%= f.text_field :label, class: "shadow-sm block w-full sm:text-sm border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2", style: "--tw-ring-color: var(--content-type-color); --tw-border-opacity: 1; border-color: var(--content-type-color);" %>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button"
|
||||
@click="document.getElementById('add-category-form').classList.add('hidden')"
|
||||
class="px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none">
|
||||
Cancel
|
||||
</button>
|
||||
<%= f.submit "Add Category", class: "px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white focus:outline-none focus:ring-2 focus:ring-offset-2", style: "background-color: var(--content-type-color); --tw-ring-color: var(--content-type-color);" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-6 pt-4 border-t border-gray-200">
|
||||
<div class="text-sm font-medium text-gray-700 mb-2">Category Suggestions</div>
|
||||
<p class="text-sm text-gray-500 mb-2">
|
||||
Notebook.ai can suggest additional categories for your pages.
|
||||
</p>
|
||||
<div class="suggest-categories-container flex flex-wrap gap-2 mb-3"></div>
|
||||
<button type="button" class="js-show-category-suggestions px-3 py-1.5 border border-gray-300 rounded-md text-xs font-medium text-gray-700 hover:bg-gray-50 focus:outline-none">
|
||||
Show Suggestions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Panel (always visible on desktop, conditionally on mobile) -->
|
||||
<div :class="{ 'hidden md:block': activePanel === 'template' && !configuring }"
|
||||
class="w-full md:w-96 lg:w-96"
|
||||
id="config-panel-container">
|
||||
|
||||
<!-- Sticky Container for Desktop/Tablet -->
|
||||
<div class="config-panel-sticky">
|
||||
|
||||
<!-- No Selection State -->
|
||||
<div x-show="!selectedCategory && !selectedField && !configuring"
|
||||
class="bg-white rounded-lg shadow-md p-6 text-center config-panel-content">
|
||||
<div class="mx-auto w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4">
|
||||
<i class="material-icons text-gray-400 text-2xl">tune</i>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Configure Your Template</h3>
|
||||
<p class="text-sm text-gray-500 mb-4">
|
||||
Select a category or field to customize it, or add a new one to get started.
|
||||
</p>
|
||||
<p class="text-xs text-gray-400 mb-1">Tip: You can drag and drop to reorder categories and fields.</p>
|
||||
<button @click="configuring = true" class="mt-2 px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none">
|
||||
<span>General Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Field Configuration -->
|
||||
<div x-show="selectedField"
|
||||
class="bg-white rounded-lg shadow-md overflow-hidden config-panel-content">
|
||||
<!-- Content will be loaded dynamically -->
|
||||
<div id="field-config-container" class="p-4">
|
||||
<div class="flex justify-center items-center p-8">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2" style="border-color: var(--content-type-color);"></div>
|
||||
<span class="ml-2 text-gray-600">Loading field settings...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Configuration -->
|
||||
<div x-show="selectedCategory"
|
||||
class="bg-white rounded-lg shadow-md overflow-hidden config-panel-content">
|
||||
<!-- Content will be loaded dynamically -->
|
||||
<div id="category-config-container" class="p-4">
|
||||
<div class="flex justify-center items-center p-8">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2" style="border-color: var(--content-type-color);"></div>
|
||||
<span class="ml-2 text-gray-600">Loading category settings...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- General Configuration -->
|
||||
<div x-show="configuring && !selectedCategory && !selectedField"
|
||||
class="bg-white rounded-lg shadow-md overflow-hidden config-panel-content">
|
||||
<%= render partial: 'content/attributes/tailwind/general_settings', locals: {
|
||||
content_type: @content_type,
|
||||
content_type_class: @content_type_class
|
||||
} %>
|
||||
</div>
|
||||
|
||||
</div> <!-- End sticky container -->
|
||||
</div> <!-- End config panel container -->
|
||||
</div> <!-- End main content area -->
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
/* Streamlined Header Styles */
|
||||
.template-header {
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.template-header:hover {
|
||||
shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* Subtle background image effect */
|
||||
.hero-bg-image {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.template-header:hover .hero-bg-image {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Content type icon styling */
|
||||
.content-type-icon-bg {
|
||||
background: linear-gradient(135deg, var(--content-type-color) 0%, color-mix(in srgb, var(--content-type-color) 80%, white) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.content-type-icon-bg:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Interactive hint styling */
|
||||
.interactive-hint {
|
||||
transition: all 0.2s ease;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.interactive-hint:hover {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.interactive-hint:hover i {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* jQuery UI Sortable placeholder styles */
|
||||
.category-placeholder {
|
||||
visibility: visible !important;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 2px dashed rgba(59, 130, 246, 0.3);
|
||||
border-radius: 8px;
|
||||
height: 120px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.field-placeholder {
|
||||
visibility: visible !important;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 2px dashed rgba(59, 130, 246, 0.3);
|
||||
border-radius: 6px;
|
||||
height: 60px;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Improve drag handle appearance */
|
||||
.category-drag-handle,
|
||||
.field-drag-handle {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.category-drag-handle:active,
|
||||
.field-drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* Category header styling */
|
||||
.category-header {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.category-header:hover {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.category-header:active {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
/* Sortable helper styles */
|
||||
.ui-sortable-helper {
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15) !important;
|
||||
transform: rotate(2deg);
|
||||
}
|
||||
|
||||
/* Notification animations */
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-notification-in {
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Loading spinner animation */
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Smart sticky configuration panel */
|
||||
.config-panel-sticky {
|
||||
position: sticky;
|
||||
top: 2rem;
|
||||
transition: all 0.2s ease-out;
|
||||
max-height: calc(100vh - 4rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* On mobile, don't make it sticky to avoid layout issues */
|
||||
@media (max-width: 768px) {
|
||||
.config-panel-sticky {
|
||||
position: static;
|
||||
max-height: none;
|
||||
overflow-y: visible;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure the configuration panels have proper scrolling */
|
||||
.config-panel-content {
|
||||
max-height: calc(100vh - 6rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.config-panel-content {
|
||||
max-height: none;
|
||||
overflow-y: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Template editor JavaScript functionality is loaded via the template_editor pack -->
|
||||
@ -338,7 +338,6 @@ Rails.application.routes.draw do
|
||||
|
||||
# Attributes
|
||||
get ':content_type/attributes', to: 'content#attributes', as: :attribute_customization
|
||||
get ':content_type/attributes-tailwind', to: 'content#attributes_tailwind', as: :attribute_customization_tailwind
|
||||
get ':content_type/template/export', to: 'content#export_template', as: :export_template
|
||||
delete ':content_type/template/reset', to: 'content#reset_template', as: :reset_template
|
||||
end
|
||||
|
||||
Loading…
Reference in New Issue
Block a user