diff --git a/app/assets/javascripts/collection_wizard.js b/app/assets/javascripts/collection_wizard.js new file mode 100644 index 00000000..d56bf30a --- /dev/null +++ b/app/assets/javascripts/collection_wizard.js @@ -0,0 +1,403 @@ +// Collection Creation Wizard Controller +document.addEventListener('DOMContentLoaded', function() { + let currentStep = 1; + const totalSteps = 4; + + // Initialize wizard + initializeWizard(); + + function initializeWizard() { + // Set up step navigation + setupStepNavigation(); + + // Set up form interactions + setupFormInteractions(); + + // Set up auto-save + setupAutoSave(); + + // Set up content type selection + setupContentTypeSelection(); + + // Set up theme selection + setupThemeSelection(); + + // Initialize preview + updatePreview(); + + // Update progress + updateProgress(); + } + + function setupStepNavigation() { + // Step navigation buttons + document.querySelectorAll('.step-nav').forEach(button => { + button.addEventListener('click', function() { + const targetStep = parseInt(this.dataset.step); + if (targetStep <= getCompletedSteps() + 1) { + goToStep(targetStep); + } + }); + }); + + // Next/Previous buttons + document.querySelectorAll('.next-step').forEach(button => { + button.addEventListener('click', function() { + if (validateCurrentStep()) { + nextStep(); + } + }); + }); + + document.querySelectorAll('.prev-step').forEach(button => { + button.addEventListener('click', function() { + previousStep(); + }); + }); + } + + function setupFormInteractions() { + // Real-time form validation and preview updates + const formInputs = document.querySelectorAll('#collection-form input, #collection-form textarea, #collection-form select'); + + formInputs.forEach(input => { + input.addEventListener('input', function() { + updatePreview(); + validateCurrentStep(); + markAsModified(); + }); + + input.addEventListener('change', function() { + updatePreview(); + validateCurrentStep(); + markAsModified(); + }); + }); + + // Character counter for description + const descriptionField = document.querySelector('textarea[name="page_collection[description]"]'); + const charCounter = document.getElementById('char-count'); + + if (descriptionField && charCounter) { + descriptionField.addEventListener('input', function() { + updateCharCount(); + }); + updateCharCount(); + } + + function updateCharCount() { + const count = descriptionField.value.length; + charCounter.textContent = `${count} / 500`; + + if (count > 450) { + charCounter.classList.add('text-red-600'); + charCounter.classList.remove('text-amber-600'); + } else if (count > 400) { + charCounter.classList.add('text-amber-600'); + charCounter.classList.remove('text-red-600'); + } else { + charCounter.classList.remove('text-amber-600', 'text-red-600'); + } + } + } + + function setupAutoSave() { + let saveTimeout; + let isModified = false; + + window.markAsModified = function() { + isModified = true; + updateSaveStatus('saving'); + + clearTimeout(saveTimeout); + saveTimeout = setTimeout(() => { + if (isModified) { + saveDraft(); + } + }, 2000); + }; + + function saveDraft() { + // Simulate auto-save (in a real app, this would make an AJAX request) + updateSaveStatus('saving'); + + setTimeout(() => { + updateSaveStatus('saved'); + isModified = false; + }, 1000); + } + + function updateSaveStatus(status) { + const saveStatus = document.getElementById('save-status'); + const saveIndicator = document.querySelector('.auto-save-indicator'); + + if (saveStatus && saveIndicator) { + saveIndicator.className = `auto-save-indicator ${status}`; + + switch(status) { + case 'saving': + saveStatus.textContent = 'Saving changes...'; + break; + case 'saved': + saveStatus.textContent = 'All changes saved'; + break; + case 'error': + saveStatus.textContent = 'Error saving changes'; + break; + } + } + } + + // Manual save draft button + const saveDraftButton = document.getElementById('save-draft'); + if (saveDraftButton) { + saveDraftButton.addEventListener('click', function() { + saveDraft(); + }); + } + } + + function setupContentTypeSelection() { + const selectAllBtn = document.getElementById('select-all-types'); + const selectNoneBtn = document.getElementById('select-none-types'); + const selectCommonBtn = document.getElementById('select-common-types'); + const checkboxes = document.querySelectorAll('.content-type-checkbox'); + + if (selectAllBtn) { + selectAllBtn.addEventListener('click', function(e) { + e.preventDefault(); + checkboxes.forEach(checkbox => { + checkbox.checked = true; + updateCheckboxUI(checkbox); + }); + updatePreview(); + markAsModified(); + }); + } + + if (selectNoneBtn) { + selectNoneBtn.addEventListener('click', function(e) { + e.preventDefault(); + checkboxes.forEach(checkbox => { + checkbox.checked = false; + updateCheckboxUI(checkbox); + }); + updatePreview(); + markAsModified(); + }); + } + + if (selectCommonBtn) { + selectCommonBtn.addEventListener('click', function(e) { + e.preventDefault(); + checkboxes.forEach(checkbox => { + checkbox.checked = checkbox.dataset.common === 'true'; + updateCheckboxUI(checkbox); + }); + updatePreview(); + markAsModified(); + }); + } + + // Update checkbox UI when changed + checkboxes.forEach(checkbox => { + checkbox.addEventListener('change', function() { + updateCheckboxUI(this); + updatePreview(); + markAsModified(); + }); + // Initialize UI + updateCheckboxUI(checkbox); + }); + } + + function updateCheckboxUI(checkbox) { + const label = checkbox.closest('label'); + const customCheckbox = label.querySelector('.checkbox-custom'); + const checkIcon = label.querySelector('.check-icon'); + + if (checkbox.checked) { + customCheckbox.classList.add('bg-blue-500', 'border-blue-500'); + customCheckbox.classList.remove('border-gray-300'); + checkIcon.classList.remove('opacity-0'); + checkIcon.classList.add('opacity-100'); + label.classList.add('ring-2', 'ring-blue-500', 'ring-opacity-20'); + } else { + customCheckbox.classList.remove('bg-blue-500', 'border-blue-500'); + customCheckbox.classList.add('border-gray-300'); + checkIcon.classList.add('opacity-0'); + checkIcon.classList.remove('opacity-100'); + label.classList.remove('ring-2', 'ring-blue-500', 'ring-opacity-20'); + } + } + + function setupThemeSelection() { + const themeOptions = document.querySelectorAll('.theme-option'); + + themeOptions.forEach(option => { + option.addEventListener('click', function() { + // Remove active class from all options + themeOptions.forEach(opt => opt.classList.remove('active')); + + // Add active class to clicked option + this.classList.add('active'); + + // Update preview theme + updatePreviewTheme(this.dataset.theme); + markAsModified(); + }); + }); + } + + function updatePreviewTheme(theme) { + const previewHeader = document.getElementById('preview-header'); + if (!previewHeader) return; + + // Remove existing theme classes + previewHeader.className = previewHeader.className.replace(/from-\w+-\d+|to-\w+-\d+/g, ''); + + // Apply new theme + switch(theme) { + case 'warm': + previewHeader.classList.add('from-orange-500', 'to-red-600'); + break; + case 'nature': + previewHeader.classList.add('from-green-500', 'to-emerald-600'); + break; + case 'monochrome': + previewHeader.classList.add('from-gray-500', 'to-gray-800'); + break; + default: + previewHeader.classList.add('from-blue-500', 'to-purple-600'); + } + } + + function goToStep(stepNumber) { + if (stepNumber < 1 || stepNumber > totalSteps) return; + + // Hide all steps + document.querySelectorAll('.form-step').forEach(step => { + step.classList.remove('active'); + }); + + // Show target step + const targetStep = document.getElementById(`step-${stepNumber}`); + if (targetStep) { + targetStep.classList.add('active'); + } + + // Update step navigation + document.querySelectorAll('.step-nav').forEach(nav => { + nav.classList.remove('active', 'completed'); + }); + + // Mark completed steps + for (let i = 1; i < stepNumber; i++) { + const nav = document.querySelector(`[data-step="${i}"]`); + if (nav) nav.classList.add('completed'); + } + + // Mark current step as active + const currentNav = document.querySelector(`[data-step="${stepNumber}"]`); + if (currentNav) currentNav.classList.add('active'); + + currentStep = stepNumber; + updateProgress(); + } + + function nextStep() { + if (currentStep < totalSteps) { + goToStep(currentStep + 1); + } + } + + function previousStep() { + if (currentStep > 1) { + goToStep(currentStep - 1); + } + } + + function validateCurrentStep() { + switch(currentStep) { + case 1: + return validateBasicInfo(); + case 2: + return true; // Visual design is optional + case 3: + return validateContentTypes(); + case 4: + return validateSettings(); + default: + return true; + } + } + + function validateBasicInfo() { + const titleInput = document.querySelector('input[name="page_collection[title]"]'); + return titleInput && titleInput.value.trim().length > 0; + } + + function validateContentTypes() { + const checkboxes = document.querySelectorAll('.content-type-checkbox'); + return Array.from(checkboxes).some(checkbox => checkbox.checked); + } + + function validateSettings() { + const privacyRadios = document.querySelectorAll('input[name="page_collection[privacy]"]'); + return Array.from(privacyRadios).some(radio => radio.checked); + } + + function getCompletedSteps() { + let completed = 0; + if (validateBasicInfo()) completed = Math.max(completed, 1); + if (validateContentTypes()) completed = Math.max(completed, 3); + if (validateSettings()) completed = Math.max(completed, 4); + return completed; + } + + function updateProgress() { + const progressBar = document.getElementById('progress-bar'); + const progressText = document.getElementById('progress-text'); + + if (progressBar && progressText) { + const progress = (currentStep / totalSteps) * 100; + progressBar.style.width = `${progress}%`; + progressText.textContent = `Step ${currentStep} of ${totalSteps}`; + } + } + + function updatePreview() { + if (window.updateCollectionPreview) { + window.updateCollectionPreview(); + } + } + + // Submission toggle + const submissionToggle = document.querySelector('.submission-toggle'); + if (submissionToggle) { + submissionToggle.addEventListener('change', function() { + updatePreview(); + markAsModified(); + }); + } + + // Keyboard navigation + document.addEventListener('keydown', function(e) { + if (e.key === 'ArrowRight' && e.ctrlKey) { + e.preventDefault(); + if (validateCurrentStep()) nextStep(); + } else if (e.key === 'ArrowLeft' && e.ctrlKey) { + e.preventDefault(); + previousStep(); + } + }); + + // Prevent accidental navigation away + window.addEventListener('beforeunload', function(e) { + const saveStatus = document.getElementById('save-status'); + if (saveStatus && saveStatus.textContent.includes('Saving')) { + e.preventDefault(); + e.returnValue = ''; + } + }); +}); \ No newline at end of file diff --git a/app/controllers/page_collections_controller.rb b/app/controllers/page_collections_controller.rb index a1dcfc46..1b698b65 100644 --- a/app/controllers/page_collections_controller.rb +++ b/app/controllers/page_collections_controller.rb @@ -9,7 +9,7 @@ class PageCollectionsController < ApplicationController before_action :require_collection_ownership, only: [:edit, :update, :destroy] - layout 'tailwind', only: [:index, :show, :edit] + layout 'tailwind', only: [:index, :new, :show, :edit] # GET /page_collections def index diff --git a/app/views/page_collections/_preview.html.erb b/app/views/page_collections/_preview.html.erb new file mode 100644 index 00000000..59cf7d1b --- /dev/null +++ b/app/views/page_collections/_preview.html.erb @@ -0,0 +1,283 @@ + +
+ + +
+
+
+

+ <%= page_collection.title.present? ? page_collection.title : "Collection Title" %> +

+

+ <%= page_collection.subtitle.present? ? page_collection.subtitle : "Collection subtitle will appear here" %> +

+
+
+ + +
+
+
+
+ collections +
+
+

+ <%= page_collection.title.present? ? page_collection.title : "My Collection" %> +

+

by <%= current_user.display_name %>

+
+
+ +
+ + public + Public + +
+
+ +
+ <%= page_collection.description.present? ? simple_format(page_collection.description) : "Your collection description will appear here. This is where you can explain what makes your collection special and what visitors can expect to find." %> +
+ + +
+
+ library_books + 0 items +
+
+ visibility + 0 views +
+
+ schedule + Just created +
+
+
+ + +
+

+ category + Accepted Content Types +

+ +
+ +
+ person + Characters +
+
+ place + Locations +
+
+ category + Items +
+
+ public + Universes +
+
+ +

+ info + Users can submit these types of content to your collection +

+
+ + +
+
+
+ inbox + Community Contributions +
+ + block + Disabled + +
+

+ This collection is not accepting submissions from other users. +

+
+ + +
+

+ grid_view + Collection Content +

+ +
+ +
+ add_circle_outline +

Content will appear here once you start adding items to your collection

+
+
+
+ + + +
+ + + \ No newline at end of file diff --git a/app/views/page_collections/_wizard_form.html.erb b/app/views/page_collections/_wizard_form.html.erb new file mode 100644 index 00000000..ba14129e --- /dev/null +++ b/app/views/page_collections/_wizard_form.html.erb @@ -0,0 +1,413 @@ +<%= form_with(model: page_collection, local: true, id: 'collection-form', data: { controller: 'collection-wizard' }) do |f| %> + + +
+ cloud_done + All changes saved +
+ + +
+
+
+
+
+ edit +
+
+

Basic Information

+

Give your collection a name and description

+
+
+
+ +
+ +
+ <%= f.label :title, class: 'block text-sm font-medium text-gray-900 mb-2' do %> + + title + Collection Title * + + <% end %> + <%= f.text_field :title, + placeholder: 'My Awesome Collection', + class: 'block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 transition-colors duration-200 text-sm', + required: true, + data: { + action: 'input->collection-wizard#updatePreview', + preview_target: 'title' + } %> +

Choose a descriptive name that captures what your collection is about

+
+ + +
+ <%= f.label :subtitle, class: 'block text-sm font-medium text-gray-900 mb-2' do %> + + subtitles + Subtitle + + <% end %> + <%= f.text_field :subtitle, + placeholder: 'A curated selection of my favorite pages', + class: 'block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 transition-colors duration-200 text-sm', + data: { + action: 'input->collection-wizard#updatePreview', + preview_target: 'subtitle' + } %> +

Optional tagline to give more context about your collection

+
+ + +
+ <%= f.label :description, class: 'block text-sm font-medium text-gray-900 mb-2' do %> + + description + Description + + <% end %> + <%= f.text_area :description, + placeholder: "Write as little or as much as you'd like! Describe what makes this collection special...", + rows: 4, + class: 'block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 transition-colors duration-200 text-sm resize-none', + data: { + action: 'input->collection-wizard#updatePreview', + preview_target: 'description' + } %> +

+ + Brief description for your collection. <strong> and <em> tags are supported. + 0 / 500 + +

+
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+ palette +
+
+

Visual Design

+

Customize how your collection looks

+
+
+
+ +
+ +
+ + +
+
+
+ cloud_upload +
+ +
+ <%= f.label :header_image, class: 'cursor-pointer' do %> + + add_photo_alternate + Choose Image + + <%= f.file_field :header_image, + class: 'sr-only', + accept: 'image/*', + data: { + action: 'change->collection-wizard#updatePreview', + preview_target: 'header_image' + } %> + <% end %> +

or drag and drop

+
+ +
+

PNG, JPG, GIF up to 10MB

+

Recommended: 1200×400px for best results

+
+
+
+
+ + +
+ + +
+
+
+ Default +
+
+
+ Warm +
+
+
+ Nature +
+
+
+ Mono +
+
+
+
+ +
+ + +
+
+
+ + +
+
+
+
+
+ category +
+
+

Content Types

+

Choose what types of content can be added

+
+
+
+ +
+ +
+ Quick select: + + | + + | + +
+ + +
+ <% Rails.application.config.content_types[:all].each do |content_type| %> +
+ <%= f.label "page_types[#{content_type.name}]", class: 'flex items-center p-3 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-50 hover:border-gray-300 transition-all duration-200 group' do %> + <%= f.check_box "page_types[#{content_type.name}]", + checked: page_collection.page_types.include?(content_type.name), + class: 'sr-only content-type-checkbox', + data: { + type: content_type.name.downcase, + common: %w[character location item universe].include?(content_type.name.downcase), + action: 'change->collection-wizard#updatePreview' + } %> + + +
+ check +
+ + +
+
+ + <%= content_type.icon %> + +
+ + <%= content_type.name.pluralize %> + +
+ <% end %> +
+ <% end %> +
+
+ +
+ + +
+
+
+ + +
+
+
+
+
+ settings +
+
+

Privacy & Settings

+

Control who can see and contribute to your collection

+
+
+
+ +
+ +
+ + + visibility + Visibility * + + + +
+ <%= f.label :privacy, class: 'flex items-start p-4 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-50 hover:border-gray-300 transition-all duration-200' do %> + <%= f.radio_button :privacy, 'public', class: 'mt-1 mr-3 text-green-600 focus:ring-green-500' %> +
+
+ public + Public +
+

Anyone can discover and view this collection

+
+ <% end %> + + <%= f.label :privacy, class: 'flex items-start p-4 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-50 hover:border-gray-300 transition-all duration-200' do %> + <%= f.radio_button :privacy, 'private', class: 'mt-1 mr-3 text-gray-600 focus:ring-gray-500' %> +
+
+ lock + Private +
+

Only you can see this collection

+
+ <% end %> +
+
+ + +
+ + + inbox + Community Contributions + + + +
+ <%= f.label :allow_submissions, class: 'flex items-start cursor-pointer' do %> + <%= f.check_box :allow_submissions, class: 'sr-only submission-toggle' %> +
+
+
Allow submissions from other users
+

Let the community suggest content for your collection

+
+ <% end %> +
+
+
+ +
+ +
+ <%= link_to page_collections_path, class: "px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors duration-200" do %> + Cancel + <% end %> + <%= f.submit 'Create Collection', class: "px-6 py-2 bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 inline-flex items-center" %> +
+
+
+
+<% end %> + + + \ No newline at end of file diff --git a/app/views/page_collections/new.html.erb b/app/views/page_collections/new.html.erb index 4f1bcd21..1e300af6 100644 --- a/app/views/page_collections/new.html.erb +++ b/app/views/page_collections/new.html.erb @@ -3,10 +3,10 @@
-
+
-