release polish

This commit is contained in:
Andrew Brown 2025-06-30 01:30:07 -07:00
parent f3e4fab02e
commit 4f1abc82cd
20 changed files with 276 additions and 54 deletions

View File

@ -1,10 +1,16 @@
class ContentAuthorizer < ApplicationAuthorizer
def readable_by? user
return true if user && user.site_administrator?
return true if ::PermissionService.user_owns_any_containing_universe?(user: user, content: resource)
return true if ::PermissionService.user_owns_content?(user: user, content: resource)
# Check public content first - these should be accessible to anyone even without a user account
return true if ::PermissionService.content_is_public?(content: resource)
return true if ::PermissionService.content_is_in_a_public_universe?(content: resource)
# If no user is provided (logged out), they can only access public content
return false if user.nil?
# Otherwise check user-specific permissions
return true if user.site_administrator?
return true if ::PermissionService.user_owns_any_containing_universe?(user: user, content: resource)
return true if ::PermissionService.user_owns_content?(user: user, content: resource)
return true if ::PermissionService.user_can_contribute_to_containing_universe?(user: user, content: resource)
return false

View File

@ -1,8 +1,14 @@
class CoreContentAuthorizer < ContentAuthorizer
def self.readable_by?(user)
return true if user && resource.user == user # PermissionService.user_owns_content?()
# Check public content first - these should be accessible to anyone
return true if resource.privacy == 'public'
return true if resource.try(:universe).try(:privacy) == 'public'
# For private content, require a user
return false if user.nil?
# Check user-specific permissions
return true if resource.user == user # PermissionService.user_owns_content?()
false
end

View File

@ -27,8 +27,10 @@ class BrowseController < ApplicationController
# Go through each content type and find public items with this tag
Rails.application.config.content_types[:all].each do |content_type|
# First find page IDs with this tag
tag_page_ids = PageTag.where(page_type: content_type.name, slug: @tag_slug).pluck(:page_id)
# First find page IDs with this tag - case insensitive matching for slug
tag_page_ids = PageTag.where(page_type: content_type.name)
.where("LOWER(slug) = ?", @tag_slug.downcase)
.pluck(:page_id)
if tag_page_ids.any?
# Use database-level randomization with the daily seed for caching potential
@ -49,7 +51,9 @@ class BrowseController < ApplicationController
end
# Add documents separately since they don't use the common content type structure
document_tag_page_ids = PageTag.where(page_type: 'Document', slug: @tag_slug).pluck(:page_id)
document_tag_page_ids = PageTag.where(page_type: 'Document')
.where("LOWER(slug) = ?", @tag_slug.downcase)
.pluck(:page_id)
if document_tag_page_ids.any?
documents = Document.where(id: document_tag_page_ids)
.where(privacy: 'public')
@ -65,7 +69,9 @@ class BrowseController < ApplicationController
end
# Add timelines separately since they don't use the common content type structure
timeline_tag_page_ids = PageTag.where(page_type: 'Timeline', slug: @tag_slug).pluck(:page_id)
timeline_tag_page_ids = PageTag.where(page_type: 'Timeline')
.where("LOWER(slug) = ?", @tag_slug.downcase)
.pluck(:page_id)
if timeline_tag_page_ids.any?
timelines = Timeline.where(id: timeline_tag_page_ids)
.where(privacy: 'public')

View File

@ -89,10 +89,10 @@ class ContentController < ApplicationController
def show
content_type = content_type_from_controller(self.class)
return redirect_to(root_path, notice: "That page doesn't exist!") unless valid_content_types.include?(content_type.name)
return redirect_to(root_path, notice: "That page doesn't exist!", status: :not_found) unless valid_content_types.include?(content_type.name)
@content = content_type.find_by(id: params[:id])
return redirect_to(root_path, notice: "You don't have permission to view that content.") if @content.nil?
return redirect_to(root_path, notice: "You don't have permission to view that content.", status: :not_found) if @content.nil?
return redirect_to(root_path) if @content.user.nil? # deleted user's content
return if ENV.key?('CONTENT_BLACKLIST') && ENV['CONTENT_BLACKLIST'].split(',').include?(@content.user.try(:email))

View File

@ -47,7 +47,7 @@ class DocumentAnalysesController < ApplicationController
def authorize_user_for_document
unless @document.present? && (current_user || User.new).can_read?(@document)
redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.")
redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.", status: :not_found)
return false
end
end

View File

@ -68,11 +68,11 @@ class DocumentsController < ApplicationController
def show
unless @document.present? && (current_user || User.new).can_read?(@document)
return redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.")
return redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.", status: :not_found)
end
if @document.user.thredded_user_detail.moderation_state == "blocked"
return redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.")
return redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.", status: :not_found)
end
# Put the focus on the document by removing Notebook.ai actions
@ -81,7 +81,7 @@ class DocumentsController < ApplicationController
def analysis
unless @document.present? && (current_user || User.new).can_read?(@document)
redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.")
redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.", status: :not_found)
end
@analysis = @document.document_analysis.where.not(queued_at: nil).order('updated_at DESC').first
@ -98,7 +98,7 @@ class DocumentsController < ApplicationController
end
def queue_analysis
return redirect_back(fallback_location: documents_path, notice: "That document doesn't exist!") unless @document.present?
return redirect_back(fallback_location: documents_path, notice: "That document doesn't exist!", status: :not_found) unless @document.present?
return redirect_back(fallback_location: documents_path, notice: "Document analysis is a feature for Premium users.") unless @document.user.on_premium_plan?
return redirect_back(fallback_location: documents_path, notice: "You don't have permission to do that!") unless @document.user == current_user
@ -214,7 +214,7 @@ class DocumentsController < ApplicationController
def plaintext
unless @document.present? && (current_user || User.new).can_read?(@document)
return redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.")
return redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.", status: :not_found)
end
render layout: 'plaintext'
@ -335,7 +335,7 @@ class DocumentsController < ApplicationController
@document = Document.find_by(id: params[:id])
unless @document
redirect_to root_path, notice: "Either that document doesn't exist or you don't have permission to view it!"
redirect_to root_path, notice: "Either that document doesn't exist or you don't have permission to view it!", status: :not_found
return
end
end

View File

@ -7,7 +7,7 @@ class UniversesController < ContentController
@content_type = content_type_name.to_s.singularize.capitalize.constantize
@universe = Universe.find_by(id: params[:id])
return redirect_to(root_path, notice: "That universe doesn't exist!") unless @universe.present?
return redirect_to(root_path, notice: "That universe doesn't exist!", status: :not_found) unless @universe.present?
@content_list = @universe.send(content_type_name)
# todo just use current_user.can_view?(@universe) and/or individual filtering

View File

@ -99,7 +99,10 @@ class UsersController < ApplicationController
@tag_slug = params[:tag_slug]
@tag = PageTag.find_by(user_id: @user.id, slug: @tag_slug)
return redirect_to(profile_by_username_path(username: @user.username), notice: 'That tag does not exist.') if @tag.nil?
if @tag.nil?
redirect_url = @user.username.present? ? profile_by_username_path(username: @user.username) : user_path(@user)
return redirect_to(redirect_url, notice: 'That tag does not exist.', status: :not_found)
end
# Find all public content with this tag
@tagged_content = []
@ -181,7 +184,7 @@ class UsersController < ApplicationController
def set_user
@user = User.find_by(user_params)
return redirect_to(root_path, notice: 'That user does not exist.') if @user.nil?
return redirect_to(root_path, notice: 'That user does not exist.', status: :not_found) if @user.nil?
return redirect_to(root_path, notice: 'That user has chosen to hide their profile.') if @user.private_profile?
return redirect_to(root_path, notice: 'That user has had their profile hidden.') if @user.thredded_user_detail.moderation_state == 'blocked'

View File

@ -25,11 +25,18 @@ class PermissionService < Service
end
def self.user_can_contribute_to_containing_universe?(user:, content:)
# Early return if no user is provided
return false if user.nil?
# Special case for attribute-related content
return true if [AttributeCategory, AttributeField, Attribute].include?(content.class) #todo audit this
return true if user.contributable_universe_ids.include?(content.universe_id)
return true if user.universes.pluck(:id).include?(content.universe_id)
# Handle cases where content might not have a universe_id
return false if content.universe_id.nil?
# Check if user can contribute to this universe
return true if user.respond_to?(:contributable_universe_ids) && user.contributable_universe_ids.include?(content.universe_id)
return true if user.respond_to?(:universes) && user.universes.pluck(:id).include?(content.universe_id)
return false
end

View File

@ -80,7 +80,7 @@
<div class="chip z-depth-1 white" style="border: none; margin: 0; font-weight: 500;">
<i class="material-icons left <%= @accent_color %>-text">category</i>
<%= pluralize(content_types_count, 'category') %>
<%= pluralize(content_types_count, 'page type') %>
</div>
<div class="chip z-depth-1 white" style="border: none; margin: 0; font-weight: 500;">
@ -106,20 +106,20 @@
<!-- Category quick filters -->
<div class="col s12">
<div class="category-filters z-depth-1" style="background-color: #fff; border-radius: 8px; padding: 16px; margin-bottom: 25px;">
<div class="category-filters z-depth-1" style="border-radius: 8px; padding: 16px; margin-bottom: 25px;">
<div class="category-filter-title" style="margin-bottom: 10px; display: flex; align-items: center;">
<i class="material-icons <%= @accent_color %>-text" style="margin-right: 8px;">filter_list</i>
<span style="font-weight: 500; font-size: 16px;">Filter by category:</span>
</div>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<a href="javascript:void(0);" onclick="return filterByCategory('all');" class="category-filter active" data-category="all" style="padding: 6px 12px; border-radius: 4px; background-color: #f5f5f5; text-decoration: none; color: #333; font-weight: 500; transition: all 0.2s ease; display: flex; align-items: center;">
<a href="javascript:void(0);" onclick="return filterByCategory('all');" class="category-filter active black-text" data-category="all" style="padding: 6px 12px; border-radius: 4px; background-color: #f5f5f5; text-decoration: none; font-weight: 500; transition: all 0.2s ease; display: flex; align-items: center;">
<i class="material-icons <%= @accent_color %>-text left" style="font-size: 18px; margin-right: 6px;">layers</i>
All (<%= total_items %>)
</a>
<% @tagged_content.each do |content_group| %>
<a href="javascript:void(0);" onclick="return filterByCategory('<%= content_group[:type].downcase %>');" class="category-filter" data-category="<%= content_group[:type].downcase %>" style="padding: 6px 12px; border-radius: 4px; background-color: #f5f5f5; text-decoration: none; color: #333; transition: all 0.2s ease; display: flex; align-items: center;">
<a href="javascript:void(0);" onclick="return filterByCategory('<%= content_group[:type].downcase %>');" class="category-filter black-text" data-category="<%= content_group[:type].downcase %>" style="padding: 6px 12px; border-radius: 4px; background-color: #f5f5f5; text-decoration: none; transition: all 0.2s ease; display: flex; align-items: center;">
<i class="material-icons <%= content_group[:color] %>-text left" style="font-size: 18px; margin-right: 6px;"><%= content_group[:icon] %></i>
<%= content_group[:type].pluralize %> (<%= content_group[:content].count %>)
</a>
@ -453,12 +453,23 @@
.modal-how-to-join {
border-radius: 8px;
max-width: 500px;
transition: background-color 1s ease;
}
body.dark .modal-how-to-join {
background-color: #2D2D31 !important;
color: #ddd;
}
.modal-how-to-join h4 {
margin-top: 0;
font-size: 20px;
font-weight: 500;
transition: color 1s ease;
}
body.dark .modal-how-to-join h4 {
color: white;
}
.modal-how-to-join ol {
@ -473,6 +484,17 @@
background-color: #f3e5f5;
padding: 2px 5px;
border-radius: 3px;
transition: background-color 1s ease, color 1s ease;
}
body.dark .modal-how-to-join .highlight {
background-color: rgba(156, 39, 176, 0.2);
color: white;
}
/* Fix modal content border */
body.dark .modal-content {
border-color: rgba(255, 255, 255, 0.1) !important;
}
:root {
@ -545,6 +567,39 @@
}
/* Category filters styling */
.category-filters {
background-color: #fff;
transition: background-color 1s ease;
}
body.dark .category-filters {
background-color: rgba(255, 255, 255, 0.1);
}
.category-filter {
background-color: #f5f5f5;
color: #333;
transition: background-color 1s ease, color 1s ease;
}
body.dark .category-filter {
background-color: rgba(255, 255, 255, 0.05);
}
/* Ensure filter text is always readable regardless of theme */
.category-filter {
color: #333 !important;
}
/* Fix the filter title text in dark mode */
body.dark .category-filter-title span {
color: #ddd;
}
body.dark .category-filter.active {
color: #333;
}
@media only screen and (max-width: 600px) {
.category-filters {
overflow-x: auto;
@ -580,10 +635,16 @@
/* Better card styling */
.hoverable.card {
transition: box-shadow 0.25s, transform 0.25s;
transition: box-shadow 0.25s, transform 0.25s, background-color 1s ease;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
border-radius: 8px;
margin-bottom: 8px;
background-color: #fff;
}
body.dark .hoverable.card {
background-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
.hoverable.card:hover {
@ -591,11 +652,20 @@
transform: translateY(-2px);
}
body.dark .hoverable.card:hover {
box-shadow: 0 5px 15px rgba(0,0,0,0.4);
}
/* Position relative for absolute positioning within */
.hoverable.card {
position: relative;
padding-bottom: 8px;
border-bottom: 4px solid #eeeeee;
transition: border-color 1s ease;
}
body.dark .hoverable.card {
border-bottom: 4px solid rgba(255, 255, 255, 0.1);
}
.js-content-card-container > a,
@ -623,6 +693,11 @@
overflow: hidden;
position: relative;
padding-bottom: 20px !important;
transition: background-color 1s ease;
}
body.dark .card .card-content {
background-color: rgba(255, 255, 255, 0.1);
}
.tags-container {
@ -653,6 +728,11 @@
height: 15px;
background: linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
pointer-events: none;
transition: background 1s ease;
}
body.dark .card .card-content:after {
background: linear-gradient(to bottom, rgba(40,40,45,0) 0%, rgba(40,40,45,1) 100%);
}
/* Card footer line - more visible */
@ -666,5 +746,24 @@
background-color: #f0f0f0;
border-radius: 0 0 8px 8px;
z-index: 1;
transition: background-color 1s ease;
}
body.dark .card::after {
background-color: rgba(255, 255, 255, 0.05);
}
/* Dark mode text styles */
body.dark .card .card-content span[style*="color: #424242"] {
color: #ddd !important;
}
body.dark .grey-text {
color: #aaa !important;
}
body.dark .tag-pill[style*="background-color: #f0f0f0"] {
background-color: rgba(255, 255, 255, 0.1) !important;
color: #aaa !important;
}
</style>

View File

@ -8,7 +8,7 @@
<% raw_model.image_uploads.each do |image| %>
<div class="row">
<div class="col s12 m3">
<%= link_to image.src(:original), class: 'z-depth-1 hoverable right', target: '_new' do %>
<%= link_to image.src(:original), class: 'z-depth-1 hoverable right card', target: '_new' do %>
<%= image_tag image.src(:small) %>
<% end %>
</div>
@ -20,9 +20,9 @@
</small>
</h5>
<div>
<%= link_to 'Open', image.src(:original), class: 'btn white black-text', target: '_new' %>
<%= link_to 'Open', image.src(:original), class: 'btn black-text', target: '_new' %>
<%= link_to 'Delete', image_deletion_path(image.id),
class: 'btn white black-text js-remove-image',
class: 'btn black-text js-remove-image',
method: 'delete',
remote: true,
data: { confirm: "Are you sure? This can't be undone." } %>
@ -35,7 +35,7 @@
<% @basil_images.each do |commission| %>
<div class="row">
<div class="col s12 m3">
<%= link_to commission.image, class: 'z-depth-1 hoverable right', target: '_new' do %>
<%= link_to commission.image, class: 'z-depth-1 hoverable right card', target: '_new' do %>
<%= image_tag commission.image, style: 'max-width: 100%' %>
<% end %>
</div>
@ -47,9 +47,9 @@
</small>
</h5>
<div>
<%= link_to 'Open', commission.image, class: 'btn white black-text', target: '_new' %>
<%= link_to 'Open', commission.image, class: 'btn black-text', target: '_new' %>
<%= link_to 'Delete', basil_delete_path(commission),
class: 'btn white black-text js-remove-image',
class: 'btn black-text js-remove-image',
method: 'delete',
remote: true,
data: { confirm: "Are you sure? This can't be undone." } %>

View File

@ -25,7 +25,7 @@
<div class="row">
<% all_images.each do |image_item| %>
<div class="col s12 m6 l4">
<div class="gallery-preview-card">
<div class="gallery-preview-card card">
<% if image_item[:type] == 'regular' %>
<%= link_to image_item[:data].src(:original), target: "_blank" do %>
<%= image_tag image_item[:data].src(:medium), class: 'gallery-preview-image' %>
@ -44,7 +44,7 @@
</div>
</div>
<% else %>
<div class="empty-gallery-message">
<div class="empty-gallery-message card-panel">
<p>No images have been added to this <%= content.class.name.downcase %> yet.</p>
</div>
<% end %>
@ -53,8 +53,8 @@
<div class="card-panel <%= content.class.color %> lighten-5 gallery-cta-panel">
<div class="row">
<div class="col s12 m8">
<h5 class="<%= content.class.text_color %> text-darken-2">View Full Gallery Experience</h5>
<p>Check out our newly designed gallery page with larger images, lightbox viewing, and an overview section perfect for sharing with others!</p>
<h5 class="<%= content.class.text_color %> text-darken-2 black-text">View Full Gallery Experience</h5>
<p class="black-text">Check out our newly designed gallery page with larger images, lightbox viewing, and an overview section perfect for sharing with others!</p>
</div>
<div class="col s12 m4 center-align cta-button-container">
<%= link_to send("gallery_#{content.class.name.downcase}_path", content), class: "btn-large waves-effect waves-light #{content.class.color}" do %>
@ -81,6 +81,7 @@
margin-bottom: 15px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
background-color: white;
}
.gallery-preview-card:hover {

View File

@ -520,6 +520,11 @@
.gallery-container {
padding: 50px 0;
background-color: #f9f9f9;
transition: background-color 1s ease;
}
body.dark .gallery-container {
background-color: #202123;
}
.gallery-grid {
@ -542,11 +547,16 @@
border-radius: 12px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0,0,0,0.08);
transition: transform 0.3s ease, box-shadow 0.3s ease;
transition: transform 0.3s ease, box-shadow 0.3s ease, background-color 1s ease;
cursor: pointer;
height: 100%;
}
body.dark .gallery-card {
background: rgba(255, 255, 255, 0.1);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.gallery-card:hover {
transform: translateY(-8px);
box-shadow: 0 15px 30px rgba(0,0,0,0.15);
@ -565,6 +575,11 @@
display: flex;
justify-content: space-between;
align-items: center;
transition: border-color 1s ease;
}
body.dark .gallery-card-info {
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.image-date {
@ -572,6 +587,11 @@
align-items: center;
color: #757575;
font-size: 0.9rem;
transition: color 1s ease;
}
body.dark .image-date {
color: #aaa;
}
.image-date i {
@ -643,11 +663,21 @@
align-items: center;
justify-content: center;
margin: 0 auto 30px;
transition: background-color 1s ease;
}
body.dark .empty-icon {
background-color: rgba(255, 255, 255, 0.1);
}
.empty-icon i {
font-size: 50px;
color: #9e9e9e;
transition: color 1s ease;
}
body.dark .empty-icon i {
color: #aaa;
}
.empty-gallery h2 {
@ -655,22 +685,42 @@
margin-bottom: 15px;
font-weight: 300;
color: #424242;
transition: color 1s ease;
}
body.dark .empty-gallery h2 {
color: #fff;
}
.empty-gallery p {
margin-bottom: 30px;
color: #757575;
transition: color 1s ease;
}
body.dark .empty-gallery p {
color: #aaa;
}
/* ===== OVERVIEW SECTION ===== */
.overview-section {
background-color: white;
padding-bottom: 80px;
transition: background-color 1s ease;
}
body.dark .overview-section {
background-color: #2D2D31;
}
.overview-background {
background-color: #ffffff;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%23<%= @content.class.hex_color.sub('#', '') %>' fill-opacity='0.03'%3E%3Cpath opacity='.5' d='M96 95h4v1h-4v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9zm-1 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9z'/%3E%3Cpath d='M6 5V0H5v5H0v1h5v94h1V6h94V5H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
transition: background-color 1s ease;
}
body.dark .overview-background {
background-color: #2D2D31;
}
.overview-header {
@ -696,6 +746,11 @@
font-weight: 300;
margin: 0 0 15px;
color: #212121;
transition: color 1s ease;
}
body.dark .overview-title {
color: #fff;
}
.overview-subtitle {
@ -724,6 +779,12 @@
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0,0,0,0.04);
transition: border-color 1s ease, box-shadow 1s ease;
}
body.dark .overview-fields {
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 2px 12px rgba(0,0,0,0.2);
}
.overview-field {
@ -739,6 +800,11 @@
right: 25px;
height: 1px;
background: #f5f5f5;
transition: background-color 1s ease;
}
body.dark .overview-field:not(:last-child)::after {
background: rgba(255, 255, 255, 0.1);
}
.field-label {
@ -748,11 +814,21 @@
font-weight: 500;
color: #9e9e9e;
margin-bottom: 8px;
transition: color 1s ease;
}
body.dark .field-label {
color: #aaa;
}
.field-value {
color: #212121;
line-height: 1.6;
transition: color 1s ease;
}
body.dark .field-value {
color: #ddd;
}
.field-value p {
@ -769,6 +845,12 @@
background: #f9f9f9;
border-radius: 10px;
color: #757575;
transition: background-color 1s ease, color 1s ease;
}
body.dark .no-overview-message {
background: rgba(255, 255, 255, 0.05);
color: #aaa;
}
.overview-cta {
@ -780,6 +862,11 @@
.related-section {
background: #f5f5f5;
padding: 70px 0;
transition: background-color 1s ease;
}
body.dark .related-section {
background: #202123;
}
.related-header {
@ -794,6 +881,11 @@
display: flex;
align-items: center;
justify-content: center;
transition: color 1s ease;
}
body.dark .related-title {
color: #fff;
}
.related-title i {

View File

@ -11,7 +11,7 @@
<div style="margin-right: 30px;">
<strong style="font-size: 1.05rem;">Participating in Art Fight 2025?</strong>
<div style="margin-top: 4px;">
<p>
<p class="black-text">
Click to add the tag below and help others find your <%= page.class.name.downcase %>!
</p>
<div style="margin-top: 8px;">
@ -19,7 +19,7 @@
<span class="new badge purple white-text hoverable" data-badge-caption="ArtFight2025" style="margin: 0;"></span>
<% end %>
</div>
<p style="margin-top: 10px; font-size: 0.9rem; color: #222;">
<p style="margin-top: 10px; font-size: 0.9rem;" class="black-text">
<i class="material-icons tiny">lock</i>
Remember to set your <%= page.class.name.downcase %> to <strong>public</strong> in the privacy toggle if you want others to see it. All pages are private by default.
</p>

View File

@ -7,7 +7,7 @@
</div>
<div class="collection-item" style="padding: 14px">
<% @popular_tags.each do |tag| %>
<%= link_to user_tag_path(username: @user.username, tag_slug: tag.slug) do %>
<%= link_to @user.username.present? ? user_tag_path(username: @user.username, tag_slug: tag.slug) : user_id_tag_path(id: @user.id, tag_slug: tag.slug) do %>
<span class="new badge blue lighten-1 left" data-badge-caption="<%= tag.tag %>"></span>
<% end %>
<% end %>

View File

@ -38,7 +38,7 @@
<div style="margin-left: 15px;">
<div>
<%= link_to profile_by_username_path(username: @user.username), class: "white-text" do %>
<%= link_to @user.username.present? ? profile_by_username_path(username: @user.username) : user_path(@user), class: "white-text" do %>
<span style="font-size: 20px; font-weight: 500; text-shadow: 0 1px 3px rgba(0,0,0,0.3); line-height: 1.2; display: block;">
<%= @user.display_name %>
</span>
@ -65,10 +65,10 @@
<div class="chip z-depth-0 <%= @accent_color %> lighten-3" style="border: none; margin: 0;">
<i class="material-icons left">category</i>
<%= pluralize(content_types_count, 'category') %>
<%= pluralize(content_types_count, 'page type') %>
</div>
<%= link_to profile_by_username_path(username: @user.username), class: "chip grey lighten-4" do %>
<%= link_to @user.username.present? ? profile_by_username_path(username: @user.username) : user_path(@user), class: "chip grey lighten-4" do %>
<i class="material-icons left">person</i>
View profile
<% end %>
@ -147,7 +147,7 @@
<% if tag.tag == @tag.tag %>
<span class="new badge <%= content_group[:color] %> left" data-badge-caption="<%= tag.tag %>"></span>
<% else %>
<%= link_to user_tag_path(username: content.user.username, tag_slug: tag.slug) do %>
<%= link_to content.user.username.present? ? user_tag_path(username: content.user.username, tag_slug: tag.slug) : user_id_tag_path(id: content.user.id, tag_slug: tag.slug) do %>
<span class="new badge grey left" data-badge-caption="<%= tag.tag %>"></span>
<% end %>
<% end %>
@ -183,7 +183,7 @@
<% if tag.tag == @tag.tag %>
<span class="new badge <%= content_group[:color] %> left" data-badge-caption="<%= tag.tag %>"></span>
<% else %>
<%= link_to user_tag_path(username: content.user.username, tag_slug: tag.slug) do %>
<%= link_to content.user.username.present? ? user_tag_path(username: content.user.username, tag_slug: tag.slug) : user_id_tag_path(id: content.user.id, tag_slug: tag.slug) do %>
<span class="new badge grey left" data-badge-caption="<%= tag.tag %>"></span>
<% end %>
<% end %>

View File

@ -110,6 +110,9 @@ Rails.application.routes.draw do
get '/@:username/followers', to: 'users#followers'
get '/@:username/following', to: 'users#following'
get '/@:username/tag/:tag_slug', to: 'users#tag', as: :user_tag
# ID-based alternative routes for users without usernames
get '/users/:id/tag/:tag_slug', to: 'users#tag', as: :user_id_tag
resources :documents do
# Document Analysis routes

View File

@ -1,9 +1,8 @@
require 'test_helper'
class NoticeDismissalControllerTest < ActionDispatch::IntegrationTest
# test "should get dismiss" do
# get notice_dismissal_dismiss_url
# assert_response :success
# end
# Tests are disabled until properly implemented
def test_should_get_root_url
skip("Test not implemented yet")
end
end

View File

@ -1,11 +1,11 @@
starter:
id: 1
id: 3
name: Starter User
email: test@test.test
selected_billing_plan_id: 1
premium:
id: 2
id: 4
name: Premium User
email: test@test.test
selected_billing_plan_id: 3

View File

@ -9,7 +9,7 @@ require "rails/test_help"
class ActiveSupport::TestCase
# Run tests in parallel with specified workers
parallelize(workers: :number_of_processors)
# parallelize(workers: :number_of_processors) # Temporarily disabled to fix test issues
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all