changes from last night

This commit is contained in:
Andrew Brown 2025-07-21 11:07:07 -07:00
parent 76dde8c0e1
commit fb761da7b6
15 changed files with 1281 additions and 96 deletions

View File

@ -47,7 +47,7 @@ class ContentController < ApplicationController
).order(:tag)
@filtered_page_tags = []
if params.key?(:slug)
@filtered_page_tags = @page_tags.where(slug: params[:slug])
@filtered_page_tags = @page_tags.where(slug: params[:slug]).uniq(&:slug)
@content.select! { |content| @filtered_page_tags.pluck(:page_id).include?(content.id) }
end
@page_tags = @page_tags.uniq(&:tag)
@ -450,6 +450,26 @@ class ContentController < ApplicationController
@serialized_content = ContentSerializer.new(@content)
return redirect_to(root_path, notice: "You don't have permission to view that content.") unless @content.updatable_by?(current_user || User.new)
# Generate changelog statistics and data
@stats = ChangelogStatsService.new(@content)
@change_intensity = @stats.change_intensity_by_week
# Get paginated change events first, then group them
page = params[:page] || 1
per_page = 20 # 20 change events per page
# Get the base query for change events (without the .last() limit)
change_events_query = ContentChangeEvent.where(
content_id: Attribute.where(
entity_type: @content.class.name,
entity_id: @content.id
),
content_type: "Attribute"
).includes(:user).order('created_at DESC')
@paginated_events = change_events_query.paginate(page: page, per_page: per_page)
@grouped_changes = group_events_by_date(@paginated_events)
if user_signed_in?
@navbar_actions << {
label: @serialized_content.name,
@ -792,6 +812,20 @@ class ContentController < ApplicationController
private
def group_events_by_date(events)
# Group events by date for timeline display
grouped = events.group_by { |event| event.created_at.to_date }
grouped.map do |date, date_events|
{
date: date,
events: date_events,
total_field_changes: date_events.sum { |event| event.changed_fields.keys.length },
users: date_events.map(&:user).compact.uniq
}
end.sort_by { |group| group[:date] }.reverse
end
def update_page_tags
tag_list = field_params.fetch('value', '').split(PageTag::SUBMISSION_DELIMITER)
current_tags = @content.page_tags.pluck(:tag)

View File

@ -0,0 +1,148 @@
# frozen_string_literal: true
class ChangelogStatsService
def initialize(content)
@content = content
@change_events = content.attribute_change_events
end
def total_changes
@change_events.sum { |event| event.changed_fields.keys.length }
end
def active_days
@change_events.map { |event| event.created_at.to_date }.uniq.length
end
def most_recent_activity
@change_events.first&.created_at
end
def creation_date
@content.created_at
end
def days_since_creation
(Date.current - creation_date.to_date).to_i
end
def most_active_field
field_counts = Hash.new(0)
@change_events.each do |event|
event.changed_fields.keys.each do |field_key|
field_counts[field_key] += 1
end
end
return nil if field_counts.empty?
most_frequent_field_id = field_counts.max_by { |_, count| count }.first
find_field_by_id(most_frequent_field_id)
end
def change_intensity_by_week
# Group changes by week for the past 12 weeks
weeks = []
(0..11).each do |i|
week_start = i.weeks.ago.beginning_of_week
week_end = week_start.end_of_week
changes_this_week = @change_events.select do |event|
event.created_at >= week_start && event.created_at <= week_end
end
weeks << {
week_start: week_start,
week_end: week_end,
change_count: changes_this_week.sum { |event| event.changed_fields.keys.length },
event_count: changes_this_week.length
}
end
weeks.reverse
end
def changes_by_day_of_week
day_counts = Hash.new(0)
@change_events.each do |event|
day_name = event.created_at.strftime('%A')
day_counts[day_name] += event.changed_fields.keys.length
end
# Return in week order
%w[Monday Tuesday Wednesday Thursday Friday Saturday Sunday].map do |day|
{ day: day, count: day_counts[day] }
end
end
def biggest_single_update
biggest_event = @change_events.max_by { |event| event.changed_fields.keys.length }
return nil unless biggest_event
{
event: biggest_event,
field_count: biggest_event.changed_fields.keys.length,
date: biggest_event.created_at
}
end
def writing_streaks
# Find consecutive days with changes
change_dates = @change_events.map { |event| event.created_at.to_date }.uniq.sort.reverse
return [] if change_dates.empty?
streaks = []
current_streak = [change_dates.first]
change_dates.each_cons(2) do |current_date, next_date|
if (current_date - next_date).to_i == 1
current_streak << next_date
else
streaks << current_streak if current_streak.length > 1
current_streak = [next_date]
end
end
streaks << current_streak if current_streak.length > 1
streaks.sort_by(&:length).reverse
end
def longest_writing_streak
streaks = writing_streaks
return nil if streaks.empty?
longest = streaks.first
{
length: longest.length,
start_date: longest.last,
end_date: longest.first
}
end
def grouped_changes_by_date
# Group changes by date for timeline display
grouped = @change_events.group_by { |event| event.created_at.to_date }
grouped.map do |date, events|
{
date: date,
events: events,
total_field_changes: events.sum { |event| event.changed_fields.keys.length },
users: events.map(&:user).compact.uniq
}
end.sort_by { |group| group[:date] }.reverse
end
private
def find_field_by_id(field_id)
# Get related attribute and field information
related_attribute = Attribute.find_by(id: @change_events.map(&:content_id).uniq)
return nil unless related_attribute
AttributeField.find_by(id: related_attribute.attribute_field_id)
end
end

View File

@ -1,20 +1,13 @@
<%= content_for :full_width_page_header do %>
<%= render partial: 'content/display/image_card_header' %>
<% end %>
<%= render partial: 'content/display/changelog', locals: { content: @serialized_content } %>
<!--
TAILWIND: use this
<div class="flex items-center">
<span class="text-green-700 font-medium mr-1">+3</span>
<span class="text-red-700 font-medium">-3</span>
<div class="ml-2 space-x-0.5 flex items-center">
<div class="bg-green-600 border-green-700 h-3.5 w-3.5 border-2"></div>
<div class="bg-green-600 border-green-700 h-3.5 w-3.5 border-2"></div>
<div class="bg-red-600 border-red-700 h-3.5 w-3.5 border-2"></div>
<div class="bg-red-600 border-red-700 h-3.5 w-3.5 border-2"></div>
<div class="bg-gray-200 border-gray-300 h-3.5 w-3.5 border-2"></div>
</div>
<!-- Beautiful Changelog Page -->
<div class="min-h-screen bg-gray-50">
<!-- Header with Stats Dashboard -->
<%= render partial: 'content/changelog/header' %>
<!-- Main Timeline -->
<%= render partial: 'content/changelog/timeline' %>
</div>
-->
<!-- Include Alpine.js for interactivity -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

View File

@ -0,0 +1,129 @@
<!-- Changes for a specific date -->
<%
# Process the events to get organized change data
changed_attributes = Attribute.where(id: events.select { |event| event.content_type == 'Attribute' }.map(&:content_id))
changed_fields = AttributeField.where(id: changed_attributes.pluck(:attribute_field_id)).includes([:attribute_category])
%>
<div class="space-y-4">
<% events.reverse.each_with_index do |change_event, event_index| %>
<%
# Skip events without users (old data)
next if change_event.user.nil?
%>
<!-- Event Time Header -->
<div class="flex items-center justify-between mb-3 <%= event_index > 0 ? 'pt-4 border-t border-gray-100' : '' %>">
<div class="flex items-center text-sm text-gray-600">
<div class="w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center mr-3">
<% if change_event.user.avatar.attached? %>
<%= image_tag change_event.user.avatar, class: "w-full h-full rounded-full object-cover" %>
<% else %>
<i class="material-icons text-gray-500 text-sm">person</i>
<% end %>
</div>
<span class="font-medium text-gray-900"><%= change_event.user.display_name %></span>
<span class="mx-2">•</span>
<span><%= change_event.created_at.strftime('%I:%M %p') %></span>
</div>
<span class="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full">
<%= pluralize(change_event.changed_fields.keys.length, 'change') %>
</span>
</div>
<!-- Field Changes -->
<div class="space-y-3">
<% change_event.changed_fields.each do |field_key, change| %>
<%
related_attribute = changed_attributes.detect { |attribute| attribute.id == change_event.content_id }
next unless related_attribute
related_field = changed_fields.detect { |field| field.id == related_attribute.attribute_field_id }
next unless related_field
related_category = related_field.attribute_category
# Skip if value didn't actually change
next if change.first == change.second
next if change.first.blank? && change.second.blank?
next if ContentChangeEvent::FIELD_IDS_TO_EXCLUDE.include?(field_key)
# Handle privacy and blank values
old_value = change.first.blank? ? ContentChangeEvent::BLANK_PLACEHOLDER : change.first.to_s
new_value = change.second.blank? ? ContentChangeEvent::BLANK_PLACEHOLDER : change.second.to_s
# Privacy check
visible_change = true
if related_field.label.start_with?('Private')
visible_change = user_signed_in? && (
(content.raw_model.is_a?(Universe) && content.user == current_user) ||
(content.respond_to?(:universe) && content.universe && content.universe.user == current_user) ||
(content.respond_to?(:universe) && content.universe.nil? && content.user == current_user)
)
end
unless visible_change
old_value = ContentChangeEvent::PRIVATE_PLACEHOLDER
new_value = ContentChangeEvent::PRIVATE_PLACEHOLDER
end
# Special handling for privacy field
if related_field.label.downcase == 'privacy'
old_value = 'private' if old_value == ContentChangeEvent::BLANK_PLACEHOLDER
new_value = 'private' if new_value == ContentChangeEvent::BLANK_PLACEHOLDER
end
%>
<!-- Individual Field Change Card -->
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
<!-- Field Header -->
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<div class="w-8 h-8 rounded-lg bg-gray-100 flex items-center justify-center mr-3">
<i class="material-icons text-gray-600 text-sm"><%= related_category.icon %></i>
</div>
<div>
<h4 class="font-medium text-gray-900"><%= related_field.label %></h4>
<p class="text-xs text-gray-500"><%= related_category.label %></p>
</div>
</div>
<span class="text-xs px-2 py-1 bg-blue-100 text-blue-800 rounded-full font-medium">
<%= change_event.action %>
</span>
</div>
<!-- Field Change Content -->
<div class="space-y-3">
<%= render partial: "content/changelog/field_change/#{related_field.field_type}",
locals: {
old_value: old_value,
new_value: new_value,
field: related_field,
change_type: change_event.action
} %>
</div>
<!-- Change Actions -->
<div class="flex items-center justify-end mt-3 pt-3 border-t border-gray-200 space-x-2">
<% unless old_value == ContentChangeEvent::BLANK_PLACEHOLDER || old_value == ContentChangeEvent::PRIVATE_PLACEHOLDER %>
<button class="text-xs text-gray-500 hover:text-gray-700 flex items-center px-2 py-1 hover:bg-gray-200 rounded transition-colors"
onclick="navigator.clipboard.writeText('<%= escape_javascript(old_value) %>')"
title="Copy previous value">
<i class="material-icons text-xs mr-1">content_copy</i>
Copy Previous
</button>
<% end %>
<% unless new_value == ContentChangeEvent::BLANK_PLACEHOLDER || new_value == ContentChangeEvent::PRIVATE_PLACEHOLDER %>
<button class="text-xs text-gray-500 hover:text-gray-700 flex items-center px-2 py-1 hover:bg-gray-200 rounded transition-colors"
onclick="navigator.clipboard.writeText('<%= escape_javascript(new_value) %>')"
title="Copy current value">
<i class="material-icons text-xs mr-1">content_copy</i>
Copy Current
</button>
<% end %>
</div>
</div>
<% end %>
</div>
<% end %>
</div>

View File

@ -0,0 +1,150 @@
<!-- Beautiful Changelog Header with Stats -->
<div class="bg-gradient-to-br from-gray-50 to-white border-b border-gray-200">
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb Navigation -->
<nav class="mb-6">
<ol class="flex items-center space-x-2 text-sm">
<li>
<%= link_to polymorphic_path(@content),
class: "text-gray-500 hover:text-gray-700 transition-colors" do %>
<i class="material-icons text-sm mr-1 <%= @serialized_content.class_text_color %>"><%= @serialized_content.class_icon %></i>
<%= @serialized_content.name %>
<% end %>
</li>
<li class="text-gray-400">
<i class="material-icons text-sm">chevron_right</i>
</li>
<li class="text-gray-900 font-medium">
Change History
</li>
</ol>
</nav>
<!-- Main Header -->
<div class="mb-8">
<div class="flex items-center mb-4">
<div class="w-12 h-12 rounded-xl <%= @serialized_content.class_color %> flex items-center justify-center shadow-lg mr-4">
<i class="material-icons text-white text-xl"><%= @serialized_content.class_icon %></i>
</div>
<div>
<h1 class="text-3xl font-bold text-gray-900 mb-1">
Your Creative Journey
</h1>
<p class="text-gray-600">
Every change you've made to <span class="font-medium"><%= @serialized_content.name %></span>
</p>
</div>
</div>
</div>
<!-- Quick Stats Cards -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<!-- Total Changes -->
<div class="bg-white rounded-xl p-6 border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
<div class="flex items-center">
<div class="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center">
<i class="material-icons text-blue-600 text-lg">edit</i>
</div>
<div class="ml-3">
<div class="text-2xl font-bold text-gray-900"><%= pluralize(@stats.total_changes, 'change') %></div>
<div class="text-sm text-gray-500">Total</div>
</div>
</div>
</div>
<!-- Active Days -->
<div class="bg-white rounded-xl p-6 border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
<div class="flex items-center">
<div class="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center">
<i class="material-icons text-green-600 text-lg">calendar_today</i>
</div>
<div class="ml-3">
<div class="text-2xl font-bold text-gray-900"><%= pluralize(@stats.active_days, 'day') %></div>
<div class="text-sm text-gray-500">Active</div>
</div>
</div>
</div>
<!-- Days Since Creation -->
<div class="bg-white rounded-xl p-6 border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
<div class="flex items-center">
<div class="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center">
<i class="material-icons text-purple-600 text-lg">schedule</i>
</div>
<div class="ml-3">
<div class="text-2xl font-bold text-gray-900"><%= pluralize(@stats.days_since_creation, 'day') %></div>
<div class="text-sm text-gray-500">Old</div>
</div>
</div>
</div>
<!-- Biggest Single Session -->
<div class="bg-white rounded-xl p-6 border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
<div class="flex items-center">
<div class="w-10 h-10 rounded-lg bg-orange-100 flex items-center justify-center">
<i class="material-icons text-orange-600 text-lg">trending_up</i>
</div>
<div class="ml-3">
<% biggest_update = @stats.biggest_single_update %>
<% field_count = biggest_update ? biggest_update[:field_count] : 0 %>
<div class="text-2xl font-bold text-gray-900">
<%= pluralize(field_count, 'change') %>
</div>
<div class="text-sm text-gray-500">Biggest Edit Session</div>
</div>
</div>
</div>
</div>
<!-- Activity Heatmap -->
<div class="bg-white rounded-xl p-6 border border-gray-200 shadow-sm">
<h3 class="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<i class="material-icons text-gray-600 mr-2">show_chart</i>
Activity Over Time
</h3>
<div class="flex items-center space-x-1">
<% @change_intensity.each do |week_data| %>
<%
intensity_level = case week_data[:change_count]
when 0 then 'bg-gray-100'
when 1..3 then 'bg-green-200'
when 4..7 then 'bg-green-400'
when 8..15 then 'bg-green-600'
else 'bg-green-800'
end
%>
<div class="w-3 h-8 rounded-sm <%= intensity_level %> border border-gray-200"
title="<%= week_data[:change_count] %> changes during week of <%= week_data[:week_start].strftime('%b %d') %>">
</div>
<% end %>
</div>
<div class="flex justify-between text-xs text-gray-500 mt-2">
<span>12 weeks ago</span>
<span>Today</span>
</div>
</div>
<!-- Writing Insights -->
<% longest_streak = @stats.longest_writing_streak %>
<% if longest_streak %>
<div class="mt-6 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-xl p-6">
<div class="flex items-center">
<div class="w-12 h-12 rounded-xl bg-blue-500 flex items-center justify-center shadow-lg mr-4">
<i class="material-icons text-white text-xl">local_fire_department</i>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900">Your Longest Writing Streak</h3>
<p class="text-gray-600">
<span class="font-bold text-blue-600"><%= longest_streak[:length] %> consecutive days</span>
from <%= longest_streak[:start_date].strftime('%B %d') %> to <%= longest_streak[:end_date].strftime('%B %d, %Y') %>
</p>
</div>
</div>
</div>
<% end %>
</div>
</div>

View File

@ -0,0 +1,188 @@
<!-- Beautiful Timeline Layout -->
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8"
x-data="{
expandedDates: new Set(),
searchQuery: '',
toggleDate(date) {
if (this.expandedDates.has(date)) {
this.expandedDates.delete(date);
} else {
this.expandedDates.add(date);
}
},
isExpanded(date) {
return this.expandedDates.has(date);
}
}"
x-init="
// Expand the first few dates by default
<% @grouped_changes.first(3).each do |group| %>
expandedDates.add('<%= group[:date].to_s %>');
<% end %>
">
<!-- Timeline Header -->
<div class="text-center mb-12">
<h2 class="text-2xl font-bold text-gray-900 mb-2">Change Timeline</h2>
<p class="text-gray-600">Your creative journey, day by day</p>
<% if @paginated_events.total_pages > 1 %>
<div class="mt-4 text-sm text-gray-500">
Showing page <%= @paginated_events.current_page %> of <%= @paginated_events.total_pages %>
(<%= @paginated_events.total_entries %> total changes)
</div>
<% end %>
</div>
<!-- Timeline Container -->
<div class="relative">
<!-- Vertical Timeline Line -->
<div class="absolute left-8 top-0 bottom-0 w-0.5 bg-gradient-to-b from-blue-400 via-purple-400 to-green-400"></div>
<% if @grouped_changes.empty? %>
<!-- Empty State -->
<div class="text-center py-16">
<div class="w-20 h-20 rounded-full bg-gray-100 flex items-center justify-center mx-auto mb-4">
<i class="material-icons text-gray-400 text-2xl">history</i>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">No Changes Yet</h3>
<p class="text-gray-600">Start editing your <%= @serialized_content.class_name.downcase %> to see your creative journey unfold here.</p>
</div>
<% else %>
<!-- Timeline Items -->
<div class="space-y-8">
<% @grouped_changes.each_with_index do |group, index| %>
<div class="relative">
<!-- Timeline Node -->
<div class="absolute left-6 w-4 h-4 bg-white border-4 border-blue-400 rounded-full shadow-sm z-10"
style="top: 1.5rem;"></div>
<!-- Date Group Card -->
<div class="ml-16 bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<!-- Date Header (Clickable) -->
<button @click="toggleDate('<%= group[:date].to_s %>')"
class="w-full px-6 py-4 text-left hover:bg-gray-50 transition-colors border-b border-gray-100">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="flex-shrink-0 mr-4">
<div class="w-12 h-12 rounded-lg bg-gradient-to-br from-blue-100 to-purple-100 flex items-center justify-center">
<i class="material-icons text-blue-600">today</i>
</div>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900">
<%= group[:date].strftime('%B %d, %Y') %>
</h3>
<div class="flex items-center text-sm text-gray-600 mt-1">
<span class="flex items-center mr-4">
<i class="material-icons text-xs mr-1">edit</i>
<%= pluralize(group[:total_field_changes], 'change') %>
</span>
<span class="flex items-center mr-4">
<i class="material-icons text-xs mr-1">event</i>
<%= pluralize(group[:events].length, 'edit session') %>
</span>
<% if group[:users].length > 1 %>
<span class="flex items-center">
<i class="material-icons text-xs mr-1">people</i>
<%= pluralize(group[:users].length, 'contributor') %>
</span>
<% end %>
</div>
</div>
</div>
<div class="flex items-center">
<div class="text-sm text-gray-500 mr-2">
<%= distance_of_time_in_words(group[:date], Date.current) %> ago
</div>
<i class="material-icons text-gray-400 transition-transform duration-200"
:class="isExpanded('<%= group[:date].to_s %>') ? 'rotate-180' : ''"
>expand_more</i>
</div>
</div>
</button>
<!-- Changes for this Date (Collapsible) -->
<div x-show="isExpanded('<%= group[:date].to_s %>')"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 -translate-y-2"
class="border-t border-gray-100">
<div class="p-6 space-y-6">
<%= render partial: 'content/changelog/date_changes',
locals: {
events: group[:events],
content: @serialized_content,
date: group[:date]
} %>
</div>
</div>
</div>
</div>
<% end %>
</div>
<% end %>
<!-- Creation Event -->
<div class="relative mt-12">
<!-- Timeline Node -->
<div class="absolute left-6 w-4 h-4 bg-white border-4 border-green-400 rounded-full shadow-sm z-10"
style="top: 1.5rem;"></div>
<!-- Creation Card -->
<div class="ml-16 bg-gradient-to-br from-green-50 to-emerald-50 rounded-xl border-2 border-green-200 p-6">
<div class="flex items-center">
<div class="w-12 h-12 rounded-lg bg-green-500 flex items-center justify-center shadow-lg mr-4">
<i class="material-icons text-white text-xl">star</i>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-1">The Beginning</h3>
<p class="text-gray-700">
<span class="font-medium"><%= @serialized_content.name %></span> was created
<% if @content.user.present? %>
by <%= link_to @content.user.display_name, @content.user, class: "font-medium text-green-700 hover:text-green-800" %>
<% end %>
<span class="text-gray-600">
<%= distance_of_time_in_words(@content.created_at, Time.current) %> ago
(<%= @content.created_at.strftime('%B %d, %Y at %I:%M %p') %>)
</span>
</p>
</div>
</div>
</div>
</div>
<!-- Pagination Controls -->
<% if @paginated_events.total_pages > 1 %>
<div class="mt-16 flex justify-center">
<div class="bg-white rounded-xl shadow-sm border border-gray-200 px-6 py-4">
<div class="flex items-center space-x-4">
<% if @paginated_events.previous_page %>
<%= link_to url_for(params.permit(:page).merge(page: @paginated_events.previous_page)),
class: "flex items-center px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors text-gray-700" do %>
<i class="material-icons text-sm mr-1">chevron_left</i>
Previous
<% end %>
<% end %>
<div class="flex items-center space-x-2 text-sm text-gray-600">
<span>Page <%= @paginated_events.current_page %> of <%= @paginated_events.total_pages %></span>
</div>
<% if @paginated_events.next_page %>
<%= link_to url_for(params.permit(:page).merge(page: @paginated_events.next_page)),
class: "flex items-center px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors text-gray-700" do %>
Next
<i class="material-icons text-sm ml-1">chevron_right</i>
<% end %>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
</div>

View File

@ -1,14 +1,41 @@
<div class="row card">
<div class="col s12 m6 l6 red lighten-4">
<%= simple_format ContentFormatterService.show(
text: old_value,
viewing_user: current_user
) %>
<!-- Modern name field diff -->
<div class="flex items-center justify-center space-x-4 py-4">
<!-- Previous Value -->
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-500">From:</span>
<span class="px-3 py-2 bg-red-50 border border-red-200 rounded-lg text-red-800 font-medium">
<% if old_value == ContentChangeEvent::BLANK_PLACEHOLDER %>
<span class="italic text-red-400">Empty</span>
<% elsif old_value == ContentChangeEvent::PRIVATE_PLACEHOLDER %>
<span class="italic text-red-400">Private</span>
<% else %>
<%= simple_format ContentFormatterService.show(
text: old_value,
viewing_user: current_user
) %>
<% end %>
</span>
</div>
<div class="col s12 m6 l6 green lighten-4">
<%= simple_format ContentFormatterService.show(
text: new_value,
viewing_user: current_user
) %>
<!-- Arrow -->
<div class="flex-shrink-0">
<i class="material-icons text-gray-400">arrow_forward</i>
</div>
<!-- Current Value -->
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-500">To:</span>
<span class="px-3 py-2 bg-green-50 border border-green-200 rounded-lg text-green-800 font-medium">
<% if new_value == ContentChangeEvent::BLANK_PLACEHOLDER %>
<span class="italic text-green-400">Empty</span>
<% elsif new_value == ContentChangeEvent::PRIVATE_PLACEHOLDER %>
<span class="italic text-green-400">Private</span>
<% else %>
<%= simple_format ContentFormatterService.show(
text: new_value,
viewing_user: current_user
) %>
<% end %>
</span>
</div>
</div>

View File

@ -1,41 +1,66 @@
<div class="row card">
<div class="col s12 m6 l6 red lighten-4">
<div class="tags-container">
<% old_value.split(PageTag::SUBMISSION_DELIMITER).each do |tag| %>
<% if user_signed_in? && @content.user == current_user %>
<%=
link_to send(
"page_tag_#{@content.class.name.downcase.pluralize}_path",
slug: PageTagService.slug_for(tag)
) do
%>
<span class="new badge <%= @content.class.color %> left" data-badge-caption="<%= tag %>"></span>
<!-- Modern tags diff display -->
<div class="space-y-4">
<!-- Previous Tags -->
<div class="space-y-2">
<div class="flex items-center text-sm text-gray-600">
<div class="w-3 h-3 bg-red-400 rounded-full mr-2"></div>
<span class="font-medium">Previous Tags</span>
</div>
<div class="bg-red-50 border border-red-200 rounded-lg p-4 min-h-16">
<% if old_value == ContentChangeEvent::BLANK_PLACEHOLDER %>
<span class="text-red-400 italic text-sm">No tags</span>
<% elsif old_value == ContentChangeEvent::PRIVATE_PLACEHOLDER %>
<span class="text-red-400 italic text-sm">Private field</span>
<% else %>
<div class="flex flex-wrap gap-2">
<% old_value.split(PageTag::SUBMISSION_DELIMITER).each do |tag| %>
<% if user_signed_in? && @content.user == current_user %>
<%= link_to send("page_tag_#{@content.class.name.downcase.pluralize}_path", slug: PageTagService.slug_for(tag)),
class: "inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800 hover:bg-red-200 transition-colors" do %>
<i class="material-icons text-xs mr-1">tag</i>
<%= tag %>
<% end %>
<% else %>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800">
<i class="material-icons text-xs mr-1">tag</i>
<%= tag %>
</span>
<% end %>
<% end %>
<% else %>
<span class="new badge <%= @content.class.color %> left" data-badge-caption="<%= tag %>"></span>
<% end %>
</div>
<% end %>
</div>
</div>
<div class="col s12 m6 l6 green lighten-4">
<div class="tags-container">
<% new_value.split(PageTag::SUBMISSION_DELIMITER).each do |tag| %>
<% if user_signed_in? && @content.user == current_user %>
<%=
link_to send(
"page_tag_#{@content.class.name.downcase.pluralize}_path",
slug: PageTagService.slug_for(tag)
) do
%>
<span class="new badge <%= @content.class.color %> left" data-badge-caption="<%= tag %>"></span>
<!-- Current Tags -->
<div class="space-y-2">
<div class="flex items-center text-sm text-gray-600">
<div class="w-3 h-3 bg-green-400 rounded-full mr-2"></div>
<span class="font-medium">Current Tags</span>
</div>
<div class="bg-green-50 border border-green-200 rounded-lg p-4 min-h-16">
<% if new_value == ContentChangeEvent::BLANK_PLACEHOLDER %>
<span class="text-green-400 italic text-sm">No tags</span>
<% elsif new_value == ContentChangeEvent::PRIVATE_PLACEHOLDER %>
<span class="text-green-400 italic text-sm">Private field</span>
<% else %>
<div class="flex flex-wrap gap-2">
<% new_value.split(PageTag::SUBMISSION_DELIMITER).each do |tag| %>
<% if user_signed_in? && @content.user == current_user %>
<%= link_to send("page_tag_#{@content.class.name.downcase.pluralize}_path", slug: PageTagService.slug_for(tag)),
class: "inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800 hover:bg-green-200 transition-colors" do %>
<i class="material-icons text-xs mr-1">tag</i>
<%= tag %>
<% end %>
<% else %>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">
<i class="material-icons text-xs mr-1">tag</i>
<%= tag %>
</span>
<% end %>
<% end %>
<% else %>
<span class="new badge <%= @content.class.color %> left" data-badge-caption="<%= tag %>"></span>
<% end %>
</div>
<% end %>
</div>
</div>
</div>

View File

@ -1,14 +1,74 @@
<div class="row card">
<div class="col s12 m6 l6 red lighten-4">
<%= simple_format ContentFormatterService.show(
text: old_value,
viewing_user: current_user
) %>
<!-- Modern side-by-side text area diff -->
<div class="grid md:grid-cols-2 gap-4">
<!-- Previous Value -->
<div class="space-y-2">
<div class="flex items-center text-sm text-gray-600">
<div class="w-3 h-3 bg-red-400 rounded-full mr-2"></div>
<span class="font-medium">Previous</span>
<% if old_value != ContentChangeEvent::BLANK_PLACEHOLDER && old_value != ContentChangeEvent::PRIVATE_PLACEHOLDER %>
<span class="ml-2 text-xs bg-red-100 text-red-700 px-2 py-0.5 rounded-full">
<%= old_value.to_s.split.length %> words
</span>
<% end %>
</div>
<div class="bg-red-50 border border-red-200 rounded-lg p-4 min-h-24">
<% if old_value == ContentChangeEvent::BLANK_PLACEHOLDER %>
<span class="text-red-400 italic text-sm">Empty</span>
<% elsif old_value == ContentChangeEvent::PRIVATE_PLACEHOLDER %>
<span class="text-red-400 italic text-sm">Private field</span>
<% else %>
<div class="prose prose-sm max-w-none prose-red">
<%= simple_format ContentFormatterService.show(
text: old_value,
viewing_user: current_user
) %>
</div>
<% end %>
</div>
</div>
<div class="col s12 m6 l6 green lighten-4">
<%= simple_format ContentFormatterService.show(
text: new_value,
viewing_user: current_user
) %>
<!-- Current Value -->
<div class="space-y-2">
<div class="flex items-center text-sm text-gray-600">
<div class="w-3 h-3 bg-green-400 rounded-full mr-2"></div>
<span class="font-medium">Current</span>
<% if new_value != ContentChangeEvent::BLANK_PLACEHOLDER && new_value != ContentChangeEvent::PRIVATE_PLACEHOLDER %>
<span class="ml-2 text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded-full">
<%= new_value.to_s.split.length %> words
</span>
<% end %>
</div>
<div class="bg-green-50 border border-green-200 rounded-lg p-4 min-h-24">
<% if new_value == ContentChangeEvent::BLANK_PLACEHOLDER %>
<span class="text-green-400 italic text-sm">Empty</span>
<% elsif new_value == ContentChangeEvent::PRIVATE_PLACEHOLDER %>
<span class="text-green-400 italic text-sm">Private field</span>
<% else %>
<div class="prose prose-sm max-w-none prose-green">
<%= simple_format ContentFormatterService.show(
text: new_value,
viewing_user: current_user
) %>
</div>
<% end %>
</div>
</div>
</div>
</div>
<!-- Word Count Change Indicator -->
<% if old_value != ContentChangeEvent::BLANK_PLACEHOLDER && old_value != ContentChangeEvent::PRIVATE_PLACEHOLDER &&
new_value != ContentChangeEvent::BLANK_PLACEHOLDER && new_value != ContentChangeEvent::PRIVATE_PLACEHOLDER %>
<%
old_word_count = old_value.to_s.split.length
new_word_count = new_value.to_s.split.length
word_diff = new_word_count - old_word_count
%>
<% if word_diff != 0 %>
<div class="mt-3 flex items-center justify-center">
<span class="text-xs px-3 py-1 rounded-full <%= word_diff > 0 ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700' %>">
<i class="material-icons text-xs mr-1"><%= word_diff > 0 ? 'add' : 'remove' %></i>
<%= word_diff.abs %> <%= 'word'.pluralize(word_diff.abs) %> <%= word_diff > 0 ? 'added' : 'removed' %>
</span>
</div>
<% end %>
<% end %>

View File

@ -1,22 +1,47 @@
<div class="row card">
<div class="col s12 m6 l6 red lighten-4">
<% if old_value.blank? %>
<p>(no universe)</p>
<% else %>
<%= simple_format ContentFormatterService.show(
text: "[[Universe-#{old_value}]]",
viewing_user: current_user
) %>
<% end %>
<!-- Modern universe field diff with special styling -->
<div class="flex items-center justify-center space-x-4 py-4">
<!-- Previous Universe -->
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-500">From:</span>
<div class="flex items-center px-3 py-2 bg-red-50 border border-red-200 rounded-lg">
<i class="material-icons text-red-600 text-sm mr-2">public</i>
<div class="text-red-800 font-medium">
<% if old_value == ContentChangeEvent::BLANK_PLACEHOLDER %>
<span class="italic text-red-400">No universe</span>
<% elsif old_value == ContentChangeEvent::PRIVATE_PLACEHOLDER %>
<span class="italic text-red-400">Private</span>
<% else %>
<%= simple_format ContentFormatterService.show(
text: "[[Universe-#{old_value}]]",
viewing_user: current_user
) %>
<% end %>
</div>
</div>
</div>
<div class="col s12 m6 l6 green lighten-4">
<% if new_value.blank? %>
<p>(no universe)</p>
<% else %>
<%= simple_format ContentFormatterService.show(
text: "[[Universe-#{new_value}]]",
viewing_user: current_user
) %>
<% end %>
<!-- Arrow -->
<div class="flex-shrink-0">
<i class="material-icons text-gray-400">arrow_forward</i>
</div>
<!-- Current Universe -->
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-500">To:</span>
<div class="flex items-center px-3 py-2 bg-green-50 border border-green-200 rounded-lg">
<i class="material-icons text-green-600 text-sm mr-2">public</i>
<div class="text-green-800 font-medium">
<% if new_value == ContentChangeEvent::BLANK_PLACEHOLDER %>
<span class="italic text-green-400">No universe</span>
<% elsif new_value == ContentChangeEvent::PRIVATE_PLACEHOLDER %>
<span class="italic text-green-400">Private</span>
<% else %>
<%= simple_format ContentFormatterService.show(
text: "[[Universe-#{new_value}]]",
viewing_user: current_user
) %>
<% end %>
</div>
</div>
</div>
</div>

View File

@ -564,7 +564,7 @@
<% @filtered_page_tags.each do |page_tag| %>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm bg-blue-100 text-blue-800">
<%= page_tag.tag %>
<%= link_to(polymorphic_path(content_type_class, params.permit(:sort, :favorite_only).merge({ slug: params.fetch(:slug, []) - [page_tag.slug] }))) do %>
<%= link_to(polymorphic_path(content_type_class, params.permit(:sort, :favorite_only).merge({ slug: Array(params.fetch(:slug, [])) - [page_tag.slug] }))) do %>
<button class="ml-1 hover:text-blue-600">
<i class="material-icons text-sm">close</i>
</button>

View File

@ -193,7 +193,7 @@ function showPageController() {
init() {
// Set initial view from URL hash if present
const hash = window.location.hash.substring(1);
if (['overview', 'details', 'gallery', 'associations', 'collections', 'feedback'].includes(hash)) {
if (['overview', 'details', 'gallery', 'associations', 'collections', 'feedback', 'privacy'].includes(hash)) {
this.currentView = hash;
if (hash !== 'overview') {
this.currentCategory = null;

View File

@ -105,6 +105,23 @@
} %>
</div>
<!-- Privacy View -->
<div x-show="currentView === 'privacy'" class="p-8">
<div class="mb-6">
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold text-gray-900 mb-2">Privacy & Sharing</h2>
<p class="text-gray-600">Manage who can see and access this <%= content.class_name.downcase %></p>
</div>
</div>
</div>
<%= render partial: 'content/show/privacy_view', locals: {
content: content,
raw_content: raw_content
} %>
</div>
</div>
<!-- Print-friendly styles -->

View File

@ -153,7 +153,7 @@
<% if raw_content.respond_to?(:privacy) %>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Visibility</span>
<button class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium transition-colors hover:bg-gray-100 <%= raw_content.privacy == 'public' ? 'bg-green-50 text-green-700 border border-green-200' : 'bg-gray-50 text-gray-700 border border-gray-200' %>"
<button @click="switchView('privacy')" class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium transition-colors hover:bg-gray-100 <%= raw_content.privacy == 'public' ? 'bg-green-50 text-green-700 border border-green-200' : 'bg-gray-50 text-gray-700 border border-gray-200' %>"
title="Click to manage privacy settings">
<% if raw_content.privacy == 'public' %>
<i class="material-icons text-sm mr-1.5">public</i>

View File

@ -0,0 +1,389 @@
<!-- Privacy Management Interface -->
<div x-data="{
selectedPrivacy: '<%= raw_content.privacy || 'private' %>',
publicType: 'standard', // 'standard', 'password', 'discoverable'
password: '',
isLoading: false,
showToast: false,
toastMessage: ''
}"
x-init="
// Update sub-options when privacy changes
$watch('selectedPrivacy', (value, oldValue) => {
if (value === 'private') {
publicType = 'standard';
}
// Auto-save when privacy changes
if (oldValue !== undefined) {
savePrivacySettings();
}
});
// Auto-save when sub-options change
$watch('publicType', () => savePrivacySettings());
$watch('password', () => {
if (password.length >= 8) {
savePrivacySettings();
}
});
// Auto-save function
savePrivacySettings() {
this.isLoading = true;
// Simulate API call for now (replace with actual form submission)
setTimeout(() => {
this.isLoading = false;
this.showToast = true;
this.toastMessage = 'Privacy settings updated';
// Auto-hide toast after 3 seconds
setTimeout(() => {
this.showToast = false;
}, 3000);
}, 500);
// TODO: Replace with actual form submission
// fetch('/content/<%= raw_content.id %>/privacy', {
// method: 'PATCH',
// headers: {
// 'Content-Type': 'application/json',
// 'X-CSRF-Token': document.querySelector('meta[name=\"csrf-token\"]').content
// },
// body: JSON.stringify({
// privacy: this.selectedPrivacy,
// password_protected: this.passwordProtected,
// discoverable: this.discoverable,
// password: this.password
// })
// });
}
"
class="max-w-4xl mx-auto space-y-8">
<!-- Toast Notification -->
<div x-show="showToast"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform translate-y-2"
x-transition:enter-end="opacity-100 transform translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 transform translate-y-0"
x-transition:leave-end="opacity-0 transform translate-y-2"
class="fixed top-4 right-4 z-50 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg flex items-center">
<i class="material-icons text-sm mr-2">check_circle</i>
<span x-text="toastMessage"></span>
</div>
<!-- Current Status Overview -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div :class="selectedPrivacy === 'public' ? 'bg-green-100' : 'bg-gray-100'"
class="w-12 h-12 rounded-lg flex items-center justify-center mr-4 transition-colors duration-200">
<i :class="selectedPrivacy === 'public' ? 'text-green-600' : 'text-gray-600'"
class="material-icons text-xl transition-colors duration-200"
x-text="selectedPrivacy === 'public' ? 'public' : 'lock'">
</i>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900 flex items-center">
Current Status
<div x-show="isLoading" class="ml-2">
<i class="material-icons text-sm text-blue-500 animate-spin">refresh</i>
</div>
</h3>
<p class="text-sm text-gray-600">
This <%= content.class_name.downcase %> is currently
<span :class="selectedPrivacy === 'public' ? 'text-green-600' : 'text-gray-600'"
class="font-medium transition-colors duration-200"
x-text="selectedPrivacy.charAt(0).toUpperCase() + selectedPrivacy.slice(1)">
</span>
<span x-show="selectedPrivacy === 'public' && publicType === 'password'" class="text-orange-600 font-medium"> + Password Protected</span>
<span x-show="selectedPrivacy === 'public' && publicType === 'discoverable'" class="text-blue-600 font-medium"> + Discoverable</span>
</p>
</div>
</div>
<div class="text-right">
<div class="text-sm text-gray-500">Effective visibility</div>
<% if raw_content.respond_to?(:universe) && raw_content.universe.present? && raw_content.universe.privacy == 'public' %>
<div class="font-medium text-green-600 flex items-center justify-end mt-1">
<span>Public</span>
<i class="material-icons text-sm ml-1 text-blue-500" title="Set by universe '<%= raw_content.universe.name %>'">info</i>
</div>
<div class="text-xs text-gray-500 mt-1">via Universe</div>
<% else %>
<div :class="selectedPrivacy === 'public' ? 'text-green-600' : 'text-gray-600'"
class="font-medium transition-colors duration-200 mt-1"
x-text="selectedPrivacy.charAt(0).toUpperCase() + selectedPrivacy.slice(1)">
</div>
<% end %>
</div>
</div>
</div>
<!-- Privacy Level Selection -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-8">
<div class="mb-8">
<h3 class="text-xl font-semibold text-gray-900 mb-3">Privacy Settings</h3>
<p class="text-gray-600">Choose who can see and access this <%= content.class_name.downcase %></p>
</div>
<div class="space-y-4">
<!-- Private Option -->
<label class="group relative cursor-pointer block">
<input type="radio"
name="privacy"
value="private"
x-model="selectedPrivacy"
class="sr-only">
<div :class="selectedPrivacy === 'private' ? 'border-blue-500 bg-blue-50 shadow-sm' : 'border-gray-200 hover:border-gray-300 hover:shadow-sm'"
class="border-2 rounded-xl p-6 transition-all duration-200 group-hover:scale-[1.01]">
<div class="flex items-start">
<div class="flex-shrink-0">
<div :class="selectedPrivacy === 'private' ? 'bg-blue-500 text-white shadow-lg' : 'bg-gray-100 text-gray-400 group-hover:bg-gray-200'"
class="w-10 h-10 rounded-full flex items-center justify-center transition-all duration-200">
<i class="material-icons text-lg">lock</i>
</div>
</div>
<div class="ml-4 flex-1">
<div class="text-lg font-semibold text-gray-900 mb-1">Private</div>
<div class="text-gray-600">Only you and collaborators can see this page</div>
</div>
<div class="flex-shrink-0 ml-4">
<div :class="selectedPrivacy === 'private' ? 'text-blue-500' : 'text-gray-300'"
class="transition-colors duration-200">
<i class="material-icons text-xl">check_circle</i>
</div>
</div>
</div>
</div>
</label>
<!-- Public Option -->
<label class="group relative cursor-pointer block">
<input type="radio"
name="privacy"
value="public"
x-model="selectedPrivacy"
class="sr-only">
<div :class="selectedPrivacy === 'public' ? 'border-blue-500 bg-blue-50 shadow-sm' : 'border-gray-200 hover:border-gray-300 hover:shadow-sm'"
class="border-2 rounded-xl p-6 transition-all duration-200 group-hover:scale-[1.01]">
<div class="flex items-start">
<div class="flex-shrink-0">
<div :class="selectedPrivacy === 'public' ? 'bg-blue-500 text-white shadow-lg' : 'bg-gray-100 text-gray-400 group-hover:bg-gray-200'"
class="w-10 h-10 rounded-full flex items-center justify-center transition-all duration-200">
<i class="material-icons text-lg">public</i>
</div>
</div>
<div class="ml-4 flex-1">
<div class="text-lg font-semibold text-gray-900 mb-1">Public</div>
<div class="text-gray-600">Anyone with the link can view this page</div>
</div>
<div class="flex-shrink-0 ml-4">
<div :class="selectedPrivacy === 'public' ? 'text-blue-500' : 'text-gray-300'"
class="transition-colors duration-200">
<i class="material-icons text-xl">check_circle</i>
</div>
</div>
</div>
</div>
</label>
</div>
<!-- Additional Privacy Options -->
<div x-show="selectedPrivacy === 'public' || selectedPrivacy === 'private'"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform -translate-y-4"
x-transition:enter-end="opacity-100 transform translate-y-0"
class="mt-8 p-6 bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl border border-gray-200">
<h4 class="text-lg font-medium text-gray-900 mb-4 flex items-center">
<i class="material-icons text-gray-600 mr-2">tune</i>
Additional Options
</h4>
<div class="space-y-4">
<!-- Public Page Types -->
<div x-show="selectedPrivacy === 'public'" class="space-y-4">
<div class="text-sm font-medium text-gray-700 mb-3">Choose public page type:</div>
<!-- Standard Public -->
<label class="group flex items-start cursor-pointer p-4 rounded-xl bg-white border-2 border-gray-200 hover:border-blue-300 hover:shadow-md transition-all duration-200"
:class="publicType === 'standard' ? 'border-blue-500 bg-blue-50' : ''">
<input type="radio"
name="publicType"
value="standard"
x-model="publicType"
class="mt-1.5 h-5 w-5 text-blue-600 focus:ring-blue-500 border-gray-300">
<div class="ml-4 flex-1">
<div class="flex items-center mb-2">
<div class="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center mr-3">
<i class="material-icons text-sm text-blue-600">public</i>
</div>
<div class="font-medium text-gray-900">Standard Public</div>
</div>
<div class="text-sm text-gray-600 ml-11">Anyone with the link can view this page</div>
</div>
</label>
<!-- Password Protected -->
<label class="group flex items-start cursor-pointer p-4 rounded-xl bg-white border-2 border-gray-200 hover:border-blue-300 hover:shadow-md transition-all duration-200"
:class="publicType === 'password' ? 'border-orange-500 bg-orange-50' : ''">
<input type="radio"
name="publicType"
value="password"
x-model="publicType"
class="mt-1.5 h-5 w-5 text-orange-600 focus:ring-orange-500 border-gray-300">
<div class="ml-4 flex-1">
<div class="flex items-center mb-2">
<div class="w-8 h-8 rounded-lg bg-orange-100 flex items-center justify-center mr-3">
<i class="material-icons text-sm text-orange-600">key</i>
</div>
<div class="font-medium text-gray-900">Password Protected</div>
<div class="text-xs text-orange-600 ml-2 px-2 py-1 bg-orange-100 rounded-md font-medium">Coming Soon</div>
</div>
<div class="text-sm text-gray-600 ml-11">Require a password to view this public page</div>
</div>
</label>
<!-- Discoverable -->
<label class="group flex items-start cursor-pointer p-4 rounded-xl bg-white border-2 border-gray-200 hover:border-blue-300 hover:shadow-md transition-all duration-200"
:class="publicType === 'discoverable' ? 'border-green-500 bg-green-50' : ''">
<input type="radio"
name="publicType"
value="discoverable"
x-model="publicType"
class="mt-1.5 h-5 w-5 text-green-600 focus:ring-green-500 border-gray-300">
<div class="ml-4 flex-1">
<div class="flex items-center mb-2">
<div class="w-8 h-8 rounded-lg bg-green-100 flex items-center justify-center mr-3">
<i class="material-icons text-sm text-green-600">search</i>
</div>
<div class="font-medium text-gray-900">Discoverable</div>
<div class="text-xs text-orange-600 ml-2 px-2 py-1 bg-orange-100 rounded-md font-medium">Coming Soon</div>
</div>
<div class="text-sm text-gray-600 ml-11">Appears in public searches and content indexes</div>
</div>
</label>
</div>
<!-- Private Page Message -->
<div x-show="selectedPrivacy === 'private'" class="text-center py-6">
<div class="flex items-center justify-center mb-3">
<div class="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center">
<i class="material-icons text-gray-500">lock</i>
</div>
</div>
<div class="text-gray-600 font-medium">
Private pages are only visible to you and collaborators.
</div>
<div class="text-sm text-gray-500 mt-1">
No additional options are available for private pages.
</div>
</div>
</div>
</div>
<!-- Password Field (shown when password protection is enabled) -->
<div x-show="publicType === 'password'"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform -translate-y-4"
x-transition:enter-end="opacity-100 transform translate-y-0"
class="mt-6 p-6 bg-gradient-to-br from-orange-50 to-orange-100 rounded-xl border border-orange-200">
<div class="flex items-center mb-4">
<div class="w-8 h-8 rounded-lg bg-orange-200 flex items-center justify-center mr-3">
<i class="material-icons text-sm text-orange-700">key</i>
</div>
<label for="password" class="text-lg font-medium text-gray-900">
Set Password
</label>
</div>
<input type="password"
id="password"
x-model="password"
placeholder="Enter a strong password"
class="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-orange-500 transition-all duration-200">
<p class="text-sm text-gray-600 mt-3 flex items-center">
<i class="material-icons text-sm text-gray-500 mr-2">info</i>
Password must be at least 8 characters long
</p>
</div>
</div>
<!-- Universe Inheritance (if applicable) -->
<% if raw_content.respond_to?(:universe) && raw_content.universe.present? %>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-8">
<div class="mb-6">
<h3 class="text-xl font-semibold text-gray-900 mb-3 flex items-center">
<i class="material-icons text-xl <%= Universe.text_color %> mr-3"><%= Universe.icon %></i>
Universe Privacy
</h3>
<p class="text-gray-600">This page belongs to a universe with its own privacy settings that can override individual page settings</p>
</div>
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 border-2 border-blue-200 rounded-xl p-6 relative overflow-hidden">
<!-- Background pattern -->
<div class="absolute inset-0 opacity-5">
<div class="absolute inset-0" style="background-image: url('data:image/svg+xml,%3Csvg width=\"40\" height=\"40\" viewBox=\"0 0 40 40\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cg fill=\"%23000\" fill-opacity=\"0.1\"%3E%3Cpath d=\"M20 20c0-5.5-4.5-10-10-10s-10 4.5-10 10 4.5 10 10 10 10-4.5 10-10zm10 0c0-5.5-4.5-10-10-10s-10 4.5-10 10 4.5 10 10 10 10-4.5 10-10z\"/%3E%3C/g%3E%3C/svg%3E');"></div>
</div>
<div class="relative flex items-start">
<div class="flex-shrink-0">
<div class="w-12 h-12 rounded-xl <%= Universe.color %> flex items-center justify-center shadow-lg">
<i class="material-icons text-lg text-white"><%= Universe.icon %></i>
</div>
</div>
<div class="ml-4 flex-1">
<div class="text-lg font-semibold text-gray-900 mb-2"><%= raw_content.universe.name %></div>
<div class="flex items-center mb-3">
<span class="text-sm text-gray-600 mr-2">Universe is currently:</span>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium <%= (raw_content.universe.privacy || 'private') == 'public' ? 'bg-green-100 text-green-800 border border-green-200' : 'bg-gray-100 text-gray-800 border border-gray-200' %>">
<i class="material-icons text-sm mr-1.5"><%= (raw_content.universe.privacy || 'private') == 'public' ? 'public' : 'lock' %></i>
<%= (raw_content.universe.privacy || 'private').capitalize %>
</span>
</div>
<% if (raw_content.universe.privacy || 'private') == 'public' %>
<div class="bg-blue-100 border border-blue-200 rounded-lg p-3 mb-4">
<div class="flex items-start">
<i class="material-icons text-blue-600 text-lg mr-2 mt-0.5">bolt</i>
<div>
<div class="text-sm font-medium text-blue-900">Universe Override Active</div>
<div class="text-xs text-blue-700 mt-1">All pages in this universe are automatically public, regardless of individual page settings</div>
</div>
</div>
</div>
<% else %>
<div class="bg-gray-100 border border-gray-200 rounded-lg p-3 mb-4">
<div class="flex items-start">
<i class="material-icons text-gray-600 text-lg mr-2 mt-0.5">info</i>
<div>
<div class="text-sm font-medium text-gray-900">Individual Page Settings Apply</div>
<div class="text-xs text-gray-700 mt-1">Since this universe is private, your individual page privacy settings determine visibility</div>
</div>
</div>
</div>
<% end %>
</div>
<div class="ml-4 flex-shrink-0">
<%= link_to raw_content.universe,
class: "inline-flex items-center px-4 py-2 bg-white border border-blue-300 rounded-lg text-sm font-medium text-blue-700 hover:bg-blue-50 hover:border-blue-400 transition-all duration-200 shadow-sm hover:shadow-md" do %>
<i class="material-icons text-sm mr-2">settings</i>
Manage Universe
<% end %>
</div>
</div>
</div>
</div>
<% end %>
<!-- Action Info -->
<div class="flex justify-center pt-8 border-t border-gray-200">
<div class="text-sm text-gray-500 flex items-center">
<i class="material-icons text-sm mr-2">info</i>
Changes are saved automatically
</div>
</div>
</div>