From 11136b91151f78aed8bb9627f53cf8b0be151e21 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Mon, 30 Jun 2025 23:10:14 -0700 Subject: [PATCH] Revert "Start free Basil month" This reverts commit b0a2e1e2e9f0a22eea8fba665e8984e8258c39c3. --- app/controllers/basil_controller.rb | 4 +- app/controllers/browse_controller.rb | 1 + app/controllers/content_controller.rb | 18 +- app/views/basil/content.html.erb | 5 - app/views/basil/index.html.erb | 5 - app/views/browse/tag.html.erb | 1466 +++++++++++++++-- .../display/attribute_value/_tags.html.erb | 14 +- 7 files changed, 1376 insertions(+), 137 deletions(-) diff --git a/app/controllers/basil_controller.rb b/app/controllers/basil_controller.rb index 3a16142a..481bd723 100644 --- a/app/controllers/basil_controller.rb +++ b/app/controllers/basil_controller.rb @@ -207,7 +207,7 @@ class BasilController < ApplicationController @in_progress_commissions = @commissions.select { |c| c.completed_at.nil? } @generated_images_count = current_user.basil_commissions.with_deleted.count - @can_request_another = (true || current_user.on_premium_plan?) || @generated_images_count < BasilService::FREE_IMAGE_LIMIT + @can_request_another = current_user.on_premium_plan? || @generated_images_count < BasilService::FREE_IMAGE_LIMIT @can_request_another = @can_request_another && @in_progress_commissions.count < BasilService::MAX_JOB_QUEUE_SIZE end @@ -454,7 +454,7 @@ class BasilController < ApplicationController def commission @generated_images_count = current_user.basil_commissions.with_deleted.count - if false && !current_user.on_premium_plan? && @generated_images_count > BasilService::FREE_IMAGE_LIMIT + if !current_user.on_premium_plan? && @generated_images_count > BasilService::FREE_IMAGE_LIMIT redirect_back fallback_location: basil_path, notice: "You've reached your free image limit. Please upgrade to generate more images." return end diff --git a/app/controllers/browse_controller.rb b/app/controllers/browse_controller.rb index 7f84cc88..3a28efa3 100644 --- a/app/controllers/browse_controller.rb +++ b/app/controllers/browse_controller.rb @@ -158,6 +158,7 @@ class BrowseController < ApplicationController # Set a default accent color for the page @accent_color = 'purple' + # Sort content types so Characters always appear first @tagged_content = @tagged_content.sort_by do |content_group| if content_group[:type] == 'Character' diff --git a/app/controllers/content_controller.rb b/app/controllers/content_controller.rb index 8ced2f79..3d3951a8 100644 --- a/app/controllers/content_controller.rb +++ b/app/controllers/content_controller.rb @@ -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 - before_action :authenticate_user!, except: [:show, :changelog, :api_sort] \ + 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 } skip_before_action :cache_most_used_page_information, only: [ @@ -430,6 +430,7 @@ class ContentController < ApplicationController # Get all images for this content with proper ordering # Only show private images to the owner or contributors is_owner_or_contributor = false + # Check if the user is the owner or a contributor if current_user.present? && (@content.user == current_user || (@content.respond_to?(:universe_id) && @content.universe_id.present? && @@ -854,6 +855,21 @@ class ContentController < ApplicationController @navbar_actions = [] return if [AttributeCategory, AttributeField].include?(content_type) + + # Set up navbar actions for gallery specifically + if action_name == 'gallery' && @content.present? + # Add a link to view the content page + @navbar_actions << { + label: @content.name, + href: polymorphic_path(@content) + } + + # Add a gallery title indicator + @navbar_actions << { + label: 'Gallery', + href: send("gallery_#{@content.class.name.downcase}_path", @content) + } + end end def set_sidenav_expansion diff --git a/app/views/basil/content.html.erb b/app/views/basil/content.html.erb index 8c84a401..2b6161cf 100644 --- a/app/views/basil/content.html.erb +++ b/app/views/basil/content.html.erb @@ -95,10 +95,6 @@ function commission_basil(style) { --> -
- Image generation is a Premium feature, but this month (June 2025), all users can generate unlimited images for free! -
- <% end %> diff --git a/app/views/basil/index.html.erb b/app/views/basil/index.html.erb index 59bf6336..0b1eb0c0 100644 --- a/app/views/basil/index.html.erb +++ b/app/views/basil/index.html.erb @@ -39,10 +39,6 @@
-
- Image generation is a Premium feature, but this month (June 2025), all users can generate unlimited images for free! -
- <% if @universe_scope %>
diff --git a/app/views/browse/tag.html.erb b/app/views/browse/tag.html.erb index a638e283..062263f7 100644 --- a/app/views/browse/tag.html.erb +++ b/app/views/browse/tag.html.erb @@ -48,16 +48,32 @@ creators_count = @tagged_content.flat_map { |group| group[:content].map(&:user_id) }.uniq.count %> - -
-
- -
+ +
+
+ + + + + + + +
-
@@ -65,38 +81,41 @@
-

+

Art Fight 2025

-

+

A showcase of creators' public content across Notebook.ai

- -
-
- <%= PageTag.icon %> - <%= pluralize(total_items, 'page') %> + +
+
+ + <%= pluralize(total_items, 'page') %>
-
- category - <%= pluralize(content_types_count, 'page type') %> +
+ + <%= pluralize(content_types_count, 'page type') %>
-
- people - <%= pluralize(creators_count, 'creator') %> +
+ + <%= pluralize(creators_count, 'creator') %>
- -
-
+ +
+
@@ -105,30 +124,71 @@
- +
-
-
- filter_list - Filter by category: +
+
<% if @tagged_content.empty? %>
@@ -139,26 +199,40 @@
<% else %> <% @tagged_content.each do |content_group| %> -
-
- <%= content_group[:icon] %> -

- <%= content_group[:type].pluralize %> - (<%= content_group[:content].count %>) -

+
+
+
+ +

+ <%= content_group[:type].pluralize %> + (<%= content_group[:content].count %>) +

+
+ +
+ + Showing <%= content_group[:content].count %> items + +
<% if content_group[:type].downcase == 'universe' %>
- <% content_group[:content].each do |content| %> + <% content_group[:content].each_with_index do |content, index| %>
- <%= link_to content do %> -
+ <%= link_to content, + class: "content-card-link", + aria: { + label: "#{content.respond_to?(:name) ? content.name : content.title} (#{content.class.name})", + describedby: "card-desc-#{content.class.name}-#{content.id}" + }, + tabindex: "0" do %> +
;">
-
+
<% # Find image for this content content_image = @random_image_pool_cache.fetch([content.class.name, content.id], []) @@ -174,51 +248,64 @@ content_image ||= asset_path("card-headers/#{content.class.name.downcase.pluralize}.jpg") %> -
+ +
- -
-
+ +
+
+
+ + +
+
<% content_name = content.respond_to?(:name) ? content.name : content.title %> <%= ContentFormatterService.show(text: content_name.presence || 'Untitled', viewing_user: current_user) %>
+ +
; color: white; border-radius: 4px; padding: 2px 6px; font-size: 10px; font-weight: 500; backdrop-filter: blur(4px); z-index: 3; box-shadow: 0 1px 3px rgba(0,0,0,0.2);"> + <%= content_group[:icon] %> + <%= content.class.name %> +
- -
+ +
<% user = @users_cache[content.user_id] %> <% if user %> -
+
-
+
<% if user.image_url %> - <%= user.display_name %> + <%= user.display_name %> <% else %> - person + person <% end %>
- <%= user.display_name %> + <%= user.display_name %>
- + update <%= time_ago_in_words content.updated_at %> ago
<% end %> - + <% if content.respond_to?(:page_tags) && content.page_tags.any? %> -
+
<% content.page_tags.each do |tag| %> <% if tag.tag == @tag %> - ; color: white; border-radius: 20px; font-size: 11px;"> + ; color: white; border-radius: 20px; font-size: 11px; font-weight: 500; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"> <%= tag.tag %> <% else %> - + <%= tag.tag %> <% end %> @@ -234,14 +321,21 @@ <% else %>
- <% content_group[:content].each do |content| %> + <% content_group[:content].each_with_index do |content, index| %>
- <%= link_to content, target: (content.is_a?(Document) ? '_blank' : nil) do %> -
+ <%= link_to content, + target: (content.is_a?(Document) ? '_blank' : nil), + class: "content-card-link", + aria: { + label: "#{content.respond_to?(:name) ? content.name : content.title} (#{content.class.name})", + describedby: "card-desc-#{content.class.name}-#{content.id}" + }, + tabindex: "0" do %> +
;">
-
+
<% # Find image for this content content_image = @random_image_pool_cache.fetch([content.class.name, content.id], []) @@ -257,51 +351,64 @@ content_image ||= asset_path("card-headers/#{content.class.name.downcase.pluralize}.jpg") %> -
+ +
- -
-
+ +
+
+
+ + +
+
<% content_name = content.respond_to?(:name) ? content.name : content.title %> <%= ContentFormatterService.show(text: content_name.presence || 'Untitled', viewing_user: current_user) %>
+ +
; color: white; border-radius: 4px; padding: 2px 6px; font-size: 10px; font-weight: 500; backdrop-filter: blur(4px); z-index: 3; box-shadow: 0 1px 3px rgba(0,0,0,0.2);"> + <%= content_group[:icon] %> + <%= content.class.name %> +
- -
+ +
<% user = @users_cache[content.user_id] %> <% if user %> -
+
-
+
<% if user.image_url %> - <%= user.display_name %> + <%= user.display_name %> <% else %> - person + person <% end %>
- <%= user.display_name %> + <%= user.display_name %>
- + update <%= time_ago_in_words content.updated_at %> ago
<% end %> - + <% if content.respond_to?(:page_tags) && content.page_tags.any? %> -
+
<% content.page_tags.each do |tag| %> <% if tag.tag == @tag %> - ; color: white; border-radius: 20px; font-size: 11px;"> + ; color: white; border-radius: 20px; font-size: 11px; font-weight: 500; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"> <%= tag.tag %> <% else %> - + <%= tag.tag %> <% end %> @@ -323,38 +430,91 @@ <%= content_for :javascript do %> - // The inline script above handles all functionality + // Handle card image loading with skeleton effect + document.addEventListener('DOMContentLoaded', function() { + // Initialize image lazy loading with skeleton effect + initLazyLoadingWithSkeletons(); + + // Initialize keyboard navigation for filter buttons + initKeyboardNavigationForFilters(); + + // Initialize mobile filter FAB behavior + initMobileFilterFAB(); + + // Initialize number counters for statistics + initNumberCounters(); + + // Add animation classes to elements + document.querySelectorAll('.js-content-card-container, .universe-card-container').forEach(function(card, index) { + // Add data attributes for sorting + const timeElement = card.querySelector('.time-info'); + if (timeElement) { + const timeText = timeElement.textContent.trim(); + let updatedAt = new Date(); + + // Parse "X time ago" format roughly + if (timeText.includes('minute')) { + const minutes = parseInt(timeText.match(/\d+/)[0] || 0); + updatedAt.setMinutes(updatedAt.getMinutes() - minutes); + } else if (timeText.includes('hour')) { + const hours = parseInt(timeText.match(/\d+/)[0] || 0); + updatedAt.setHours(updatedAt.getHours() - hours); + } else if (timeText.includes('day')) { + const days = parseInt(timeText.match(/\d+/)[0] || 0); + updatedAt.setDate(updatedAt.getDate() - days); + } else if (timeText.includes('month')) { + const months = parseInt(timeText.match(/\d+/)[0] || 0); + updatedAt.setMonth(updatedAt.getMonth() - months); + } else if (timeText.includes('year')) { + const years = parseInt(timeText.match(/\d+/)[0] || 0); + updatedAt.setFullYear(updatedAt.getFullYear() - years); + } + + card.setAttribute('data-updated-at', updatedAt.toISOString()); + } + + // Stagger animation delay for cards + card.style.opacity = '0'; + card.style.transform = 'translateY(20px)'; + card.style.transition = 'opacity 0.5s ease, transform 0.5s ease'; + + // Stagger entrance animations + setTimeout(function() { + card.style.opacity = '1'; + card.style.transform = 'translateY(0)'; + }, 50 + (index * 30)); // 30ms delay between each card's animation + }); + + // Handle parallax effect on header + initHeaderParallax(); + + // Handle focus trapping in visible content sections + setupFocusManagement(); + + }); + + // Initialize number counters with animation + function initNumberCounters() { + const counters = document.querySelectorAll('.counter-value'); + + counters.forEach(counter => { + const finalCount = parseInt(counter.getAttribute('data-count'), 10); + const originalText = counter.textContent; + const suffix = originalText.replace(/^\d+\s*/, ''); // Extract text after the number + + // Only animate if it's a reasonable number to count up to + if (finalCount <= 300) { + let count = 0; + const duration = 1500; // ms + const interval = Math.max(10, duration / finalCount); + + setTimeout(() => { + const timer = setInterval(() => { + count++; + counter.textContent = count + ' ' + suffix; + + if (count >= finalCount) { + counter.textContent = originalText; // Reset to original for correct pluralization + clearInterval(timer); + } + }, interval); + }, 500); // Delay start slightly to ensure elements are visible + } + }); + } + + // Initialize header parallax effect + function initHeaderParallax() { + const header = document.querySelector('.artfight-header'); + const background = document.querySelector('.animated-background'); + + if (header && background) { + window.addEventListener('scroll', function() { + const scrollPosition = window.scrollY; + if (scrollPosition < 500) { // Only apply effect near the top of the page + const translateY = scrollPosition * 0.15; + background.style.transform = `scale(1.1) translateY(${translateY}px)`; + } + }); + } + } + + // Initialize mobile filter FAB behavior + function initMobileFilterFAB() { + const filterSection = document.querySelector('.category-filters'); + const filterFab = document.getElementById('mobile-filter-fab'); + const isMobile = window.innerWidth <= 768; + + if (filterSection && filterFab && isMobile) { + // Show FAB when filter section is out of view + window.addEventListener('scroll', function() { + const filterRect = filterSection.getBoundingClientRect(); + + // If filter section is above viewport, show the FAB + if (filterRect.bottom < 0) { + filterFab.style.display = 'block'; + + // Animate in + filterFab.style.transform = 'scale(1)'; + filterFab.style.opacity = '1'; + } else { + filterFab.style.transform = 'scale(0)'; + filterFab.style.opacity = '0'; + + // Hide after animation completes + setTimeout(function() { + if (filterRect.bottom >= 0) { + filterFab.style.display = 'none'; + } + }, 300); + } + }); + } + } + + // Function to scroll back to filters when FAB is clicked + function showMobileFilters() { + const filterSection = document.querySelector('.category-filters'); + if (filterSection) { + // Scroll the filter section into view with smooth animation + filterSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + } + + // Initialize keyboard navigation for filter buttons + function initKeyboardNavigationForFilters() { + const filterButtonsContainer = document.querySelector('.filter-buttons-container'); + const filterButtons = Array.from(document.querySelectorAll('.category-filter')); + + if (filterButtonsContainer && filterButtons.length > 0) { + // Add keyboard navigation for tab list + filterButtonsContainer.addEventListener('keydown', function(e) { + // Find index of the current focused button + const focusedIndex = filterButtons.indexOf(document.activeElement); + + if (focusedIndex >= 0) { + let nextIndex; + + switch (e.key) { + case 'ArrowRight': + case 'ArrowDown': + e.preventDefault(); + nextIndex = (focusedIndex + 1) % filterButtons.length; + filterButtons[nextIndex].focus(); + break; + + case 'ArrowLeft': + case 'ArrowUp': + e.preventDefault(); + nextIndex = (focusedIndex - 1 + filterButtons.length) % filterButtons.length; + filterButtons[nextIndex].focus(); + break; + + case 'Home': + e.preventDefault(); + filterButtons[0].focus(); + break; + + case 'End': + e.preventDefault(); + filterButtons[filterButtons.length - 1].focus(); + break; + + case 'Enter': + case ' ': + e.preventDefault(); + document.activeElement.click(); + break; + } + } + }); + } + } + + // Handle focus management for better accessibility + function setupFocusManagement() { + // Ensure card links have appropriate focus styles + const cardLinks = document.querySelectorAll('.content-card-link'); + + cardLinks.forEach(link => { + // Set proper focus styles + link.addEventListener('focus', function() { + this.querySelector('.improved-card').classList.add('focus-visible'); + }); + + link.addEventListener('blur', function() { + this.querySelector('.improved-card').classList.remove('focus-visible'); + }); + }); + } + + // Initialize lazy loading with skeleton effect + function initLazyLoadingWithSkeletons() { + // Set up intersection observer for lazy loading + const imgObserver = new IntersectionObserver(function(entries, observer) { + entries.forEach(function(entry) { + if (entry.isIntersecting) { + const container = entry.target; + const imageBg = container.querySelector('.card-image-bg'); + const skeletonLoader = container.querySelector('.skeleton-loading'); + + if (imageBg && skeletonLoader) { + // Show skeleton while loading + skeletonLoader.style.opacity = '1'; + + // Create a new image to test loading + const img = new Image(); + img.src = imageBg.getAttribute('data-src'); + + img.onload = function() { + // Hide skeleton when loaded + skeletonLoader.style.opacity = '0'; + + // Add loaded class for any additional styling + container.classList.add('image-loaded'); + + // Stop observing this element + observer.unobserve(container); + }; + + img.onerror = function() { + // Hide skeleton even if there's an error + skeletonLoader.style.opacity = '0'; + observer.unobserve(container); + }; + } + } + }); + }, { + rootMargin: '100px', // Load images when they're within 100px of viewport + threshold: 0.1 + }); + + // Observe all image containers + document.querySelectorAll('.loading-container').forEach(function(container) { + imgObserver.observe(container); + }); + } <% end %> + + + \ No newline at end of file diff --git a/app/views/content/display/attribute_value/_tags.html.erb b/app/views/content/display/attribute_value/_tags.html.erb index 09fd1ce1..b59e0010 100644 --- a/app/views/content/display/attribute_value/_tags.html.erb +++ b/app/views/content/display/attribute_value/_tags.html.erb @@ -15,7 +15,19 @@ <% end %> <% else %> - <%= link_to user_tag_path(username: content.user.username, tag_slug: PageTagService.slug_for(tag)) do %> + <% + # Use browse_tag_path which doesn't require username if it's a globally browsable tag like artfight2025 + if PageTagService.slug_for(tag).downcase == 'artfight2025' + path = browse_tag_path(tag_slug: 'artfight2025') + elsif content.user.present? && content.user.username.present? + # Fall back to user_tag_path if username is available + path = user_tag_path(username: content.user.username, tag_slug: PageTagService.slug_for(tag)) + else + # If neither condition is met, link to the page_tag route for this content type + path = send("page_tag_#{raw_model.class.name.downcase.pluralize}_path", slug: PageTagService.slug_for(tag)) + end + %> + <%= link_to path do %> <% end %> <% end %>