mirror of
https://github.com/indentlabs/notebook.git
synced 2025-10-26 11:19:22 +00:00
515 lines
22 KiB
Plaintext
515 lines
22 KiB
Plaintext
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<div class="flex items-center justify-between mb-8">
|
|
<div class="flex items-center">
|
|
<%= link_to data_vault_path, class: 'text-gray-500 hover:text-gray-700 mr-3', title: "Back to your Data Vault" do %>
|
|
<i class="material-icons">arrow_back</i>
|
|
<% end %>
|
|
<h1 class="text-3xl font-bold text-gray-700">Notebook Analytics</h1>
|
|
</div>
|
|
<div class="text-sm text-gray-500">
|
|
Member since <%= current_user.created_at.strftime("%B %Y") %>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Key Metrics Dashboard -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
<div class="bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg shadow-md p-6 text-white">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<p class="text-blue-100 text-sm font-medium">Total Pages</p>
|
|
<h3 class="text-4xl font-bold mt-1">
|
|
<%= number_with_delimiter @content.values.sum(&:count) %>
|
|
</h3>
|
|
</div>
|
|
<div class="bg-white/20 rounded-full p-2">
|
|
<i class="material-icons">auto_stories</i>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 text-sm text-blue-100">
|
|
<%
|
|
recent_pages = 0
|
|
@content.each do |_, pages|
|
|
recent_pages += pages.select { |page| page.created_at > 30.days.ago }.count
|
|
end
|
|
%>
|
|
<% if recent_pages > 0 %>
|
|
<span class="font-medium"><%= recent_pages %> new</span> in the last 30 days
|
|
<% else %>
|
|
No new pages in the last 30 days
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg shadow-md p-6 text-white">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<p class="text-purple-100 text-sm font-medium">Total Updates</p>
|
|
<h3 class="text-4xl font-bold mt-1">
|
|
<%= number_with_delimiter current_user.content_change_events.count %>
|
|
</h3>
|
|
</div>
|
|
<div class="bg-white/20 rounded-full p-2">
|
|
<i class="material-icons">edit</i>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 text-sm text-purple-100">
|
|
<% recent_updates = current_user.content_change_events.where('created_at > ?', 30.days.ago).count %>
|
|
<% if recent_updates > 0 %>
|
|
<span class="font-medium"><%= recent_updates %> updates</span> in the last 30 days
|
|
<% else %>
|
|
No updates in the last 30 days
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gradient-to-br from-green-500 to-green-600 rounded-lg shadow-md p-6 text-white">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<p class="text-green-100 text-sm font-medium">Active Days</p>
|
|
<h3 class="text-4xl font-bold mt-1">
|
|
<%= number_with_delimiter current_user.content_change_events.group_by_day(:created_at).count %>
|
|
</h3>
|
|
</div>
|
|
<div class="bg-white/20 rounded-full p-2">
|
|
<i class="material-icons">calendar_today</i>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 text-sm text-green-100">
|
|
<%
|
|
streak_days = 0
|
|
current_date = Date.today
|
|
|
|
while current_user.content_change_events.where('DATE(created_at) = ?', current_date).exists?
|
|
streak_days += 1
|
|
current_date = current_date - 1.day
|
|
end
|
|
%>
|
|
<% if streak_days > 0 %>
|
|
Current streak: <span class="font-medium"><%= streak_days %> days</span>
|
|
<% else %>
|
|
No current streak
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gradient-to-br from-amber-500 to-amber-600 rounded-lg shadow-md p-6 text-white">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<p class="text-amber-100 text-sm font-medium">Most Active Content</p>
|
|
<%
|
|
# Find most active content type based on number of pages
|
|
most_active_type = nil
|
|
most_active_count = 0
|
|
|
|
@content.each do |type, pages|
|
|
if pages.count > most_active_count
|
|
most_active_type = type
|
|
most_active_count = pages.count
|
|
end
|
|
end
|
|
%>
|
|
<h3 class="text-2xl font-bold mt-1 truncate">
|
|
<%= most_active_type.pluralize %>
|
|
</h3>
|
|
</div>
|
|
<div class="bg-white/20 rounded-full p-2">
|
|
<i class="material-icons">trending_up</i>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 text-sm text-amber-100">
|
|
<span class="font-medium"><%= number_with_delimiter most_active_count %> pages</span> of this type
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Activity Trends -->
|
|
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-8">
|
|
<div class="p-6 border-b border-gray-200">
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<h2 class="text-xl font-semibold text-gray-800 mb-1">Activity Trends</h2>
|
|
<p class="text-gray-600">
|
|
Track how your worldbuilding activity changes over time
|
|
</p>
|
|
</div>
|
|
<div class="text-sm text-gray-500">
|
|
Every field edit counts as one update
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="border-b border-gray-200">
|
|
<div class="flex overflow-x-auto">
|
|
<a href="#edits-this-week" class="px-4 py-2 font-medium text-sm border-b-2 border-blue-500 text-blue-600 whitespace-nowrap tab-link" data-tab="edits-this-week">
|
|
This week
|
|
</a>
|
|
<a href="#edits-this-month" class="px-4 py-2 font-medium text-sm border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap tab-link" data-tab="edits-this-month">
|
|
This month
|
|
</a>
|
|
<a href="#edits-this-year" class="px-4 py-2 font-medium text-sm border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap tab-link" data-tab="edits-this-year">
|
|
This year
|
|
</a>
|
|
<a href="#edits-all-time" class="px-4 py-2 font-medium text-sm border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap tab-link" data-tab="edits-all-time">
|
|
All time
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-6 bg-gray-50">
|
|
<div id="edits-this-week" class="tab-content">
|
|
<% weekly_data = current_user.content_change_events.where('created_at > ?', DateTime.now - 7.days).group_by_day(:created_at).map { |date, count| [date.split(' ').first, count] } %>
|
|
<%= area_chart weekly_data, download: true, colors: ["#3b82f6"], points: false %>
|
|
|
|
<div class="mt-4 grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div class="bg-blue-50 rounded-lg p-4 border border-blue-100">
|
|
<div class="text-sm text-gray-500 mb-1">Total Updates</div>
|
|
<div class="text-2xl font-bold text-gray-800"><%= number_with_delimiter weekly_data.sum { |_, count| count } %></div>
|
|
</div>
|
|
<div class="bg-blue-50 rounded-lg p-4 border border-blue-100">
|
|
<div class="text-sm text-gray-500 mb-1">Most Active Day</div>
|
|
<% most_active = weekly_data.max_by { |_, count| count } %>
|
|
<div class="text-2xl font-bold text-gray-800">
|
|
<%= most_active ? Date.parse(most_active[0]).strftime("%A") : "N/A" %>
|
|
</div>
|
|
</div>
|
|
<div class="bg-blue-50 rounded-lg p-4 border border-blue-100">
|
|
<div class="text-sm text-gray-500 mb-1">Daily Average</div>
|
|
<div class="text-2xl font-bold text-gray-800">
|
|
<%= weekly_data.any? ? (weekly_data.sum { |_, count| count }.to_f / weekly_data.size).round : 0 %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="edits-this-month" class="tab-content hidden">
|
|
<% monthly_data = current_user.content_change_events.where('created_at > ?', DateTime.now - 1.month).group_by_week(:created_at).map { |date, count| [date.split(' ').first, count] } %>
|
|
<%= area_chart monthly_data, download: true, colors: ["#8b5cf6"] %>
|
|
|
|
<div class="mt-4 grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div class="bg-purple-50 rounded-lg p-4 border border-purple-100">
|
|
<div class="text-sm text-gray-500 mb-1">Total Updates</div>
|
|
<div class="text-2xl font-bold text-gray-800"><%= number_with_delimiter monthly_data.sum { |_, count| count } %></div>
|
|
</div>
|
|
<div class="bg-purple-50 rounded-lg p-4 border border-purple-100">
|
|
<div class="text-sm text-gray-500 mb-1">Most Active Week</div>
|
|
<% most_active = monthly_data.max_by { |_, count| count } %>
|
|
<div class="text-2xl font-bold text-gray-800">
|
|
<%= most_active ? "Week of #{Date.parse(most_active[0]).strftime("%b %d")}" : "N/A" %>
|
|
</div>
|
|
</div>
|
|
<div class="bg-purple-50 rounded-lg p-4 border border-purple-100">
|
|
<div class="text-sm text-gray-500 mb-1">Weekly Average</div>
|
|
<div class="text-2xl font-bold text-gray-800">
|
|
<%= monthly_data.any? ? (monthly_data.sum { |_, count| count }.to_f / monthly_data.size).round : 0 %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="edits-this-year" class="tab-content hidden">
|
|
<% yearly_data = current_user.content_change_events.where('created_at > ?', DateTime.now - 1.year).group_by_month(:created_at).map { |date, count| [date.split(' ').first, count] } %>
|
|
<%= area_chart yearly_data, download: true, colors: ["#10b981"] %>
|
|
|
|
<div class="mt-4 grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div class="bg-green-50 rounded-lg p-4 border border-green-100">
|
|
<div class="text-sm text-gray-500 mb-1">Total Updates</div>
|
|
<div class="text-2xl font-bold text-gray-800"><%= number_with_delimiter yearly_data.sum { |_, count| count } %></div>
|
|
</div>
|
|
<div class="bg-green-50 rounded-lg p-4 border border-green-100">
|
|
<div class="text-sm text-gray-500 mb-1">Most Active Month</div>
|
|
<% most_active = yearly_data.max_by { |_, count| count } %>
|
|
<div class="text-2xl font-bold text-gray-800">
|
|
<%= most_active ? Date.parse(most_active[0]).strftime("%B") : "N/A" %>
|
|
</div>
|
|
</div>
|
|
<div class="bg-green-50 rounded-lg p-4 border border-green-100">
|
|
<div class="text-sm text-gray-500 mb-1">Monthly Average</div>
|
|
<div class="text-2xl font-bold text-gray-800">
|
|
<%= yearly_data.any? ? (yearly_data.sum { |_, count| count }.to_f / yearly_data.size).round : 0 %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="edits-all-time" class="tab-content hidden">
|
|
<% all_time_data = current_user.content_change_events.group_by_month(:created_at).map { |date, count| [date.split(' ').first, count] } %>
|
|
<%= area_chart all_time_data, download: true, colors: ["#f59e0b"] %>
|
|
|
|
<div class="mt-4 grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div class="bg-amber-50 rounded-lg p-4 border border-amber-100">
|
|
<div class="text-sm text-gray-500 mb-1">Total Updates</div>
|
|
<div class="text-2xl font-bold text-gray-800"><%= number_with_delimiter all_time_data.sum { |_, count| count } %></div>
|
|
</div>
|
|
<div class="bg-amber-50 rounded-lg p-4 border border-amber-100">
|
|
<div class="text-sm text-gray-500 mb-1">Most Active Month</div>
|
|
<% most_active = all_time_data.max_by { |_, count| count } %>
|
|
<div class="text-2xl font-bold text-gray-800">
|
|
<%= most_active ? Date.parse(most_active[0]).strftime("%B %Y") : "N/A" %>
|
|
</div>
|
|
</div>
|
|
<div class="bg-amber-50 rounded-lg p-4 border border-amber-100">
|
|
<div class="text-sm text-gray-500 mb-1">Active Months</div>
|
|
<div class="text-2xl font-bold text-gray-800">
|
|
<%= all_time_data.count %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content Distribution -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
|
<!-- Content Categories -->
|
|
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
|
<div class="p-6 border-b border-gray-200">
|
|
<h2 class="text-xl font-semibold text-gray-800 mb-1">Content Categories</h2>
|
|
<p class="text-gray-600">
|
|
Your content organized by category
|
|
</p>
|
|
</div>
|
|
|
|
<div class="p-6 bg-gray-50">
|
|
<%
|
|
# Group content types into categories
|
|
categories = {
|
|
"Characters" => ["Character", "Person"],
|
|
"Locations" => ["Location", "Town", "Country", "Landmark", "Building", "Region", "Planet"],
|
|
"Items" => ["Item", "Creature", "Flora", "Vehicle", "Magic", "Technology"],
|
|
"Story Elements" => ["Scene", "Group", "Tradition", "Government", "Religion", "Race", "Deity", "Language"],
|
|
"Other" => []
|
|
}
|
|
|
|
category_counts = {}
|
|
|
|
@content.each do |type, pages|
|
|
found_category = false
|
|
categories.each do |category, types|
|
|
if types.include?(type)
|
|
category_counts[category] ||= 0
|
|
category_counts[category] += pages.count
|
|
found_category = true
|
|
break
|
|
end
|
|
end
|
|
|
|
unless found_category
|
|
category_counts["Other"] ||= 0
|
|
category_counts["Other"] += pages.count
|
|
end
|
|
end
|
|
|
|
# Remove empty categories
|
|
category_counts.delete_if { |_, count| count == 0 }
|
|
%>
|
|
|
|
<%= pie_chart category_counts, download: true, legend: "right", donut: true %>
|
|
|
|
<div class="mt-6 space-y-4">
|
|
<% category_counts.sort_by { |_, count| -count }.each do |category, count| %>
|
|
<div class="flex items-center">
|
|
<div class="w-1/3 font-medium text-gray-700"><%= category %></div>
|
|
<div class="w-2/3">
|
|
<div class="flex items-center">
|
|
<div class="flex-grow h-2 bg-gray-200 rounded-full overflow-hidden">
|
|
<div class="h-full bg-blue-600" style="width: <%= (count.to_f / category_counts.values.sum * 100).round %>%"></div>
|
|
</div>
|
|
<span class="ml-2 text-sm text-gray-600"><%= count %> (<%= (count.to_f / category_counts.values.sum * 100).round %>%)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Top Content Types -->
|
|
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
|
<div class="p-6 border-b border-gray-200">
|
|
<h2 class="text-xl font-semibold text-gray-800 mb-1">Top Content Types</h2>
|
|
<p class="text-gray-600">
|
|
Your most frequently created content types
|
|
</p>
|
|
</div>
|
|
|
|
<div class="p-6 bg-gray-50">
|
|
<%
|
|
# Get top 10 content types by count
|
|
top_content_types = @content.sort_by { |_, pages| -pages.count }.first(10).to_h
|
|
%>
|
|
|
|
<%= bar_chart top_content_types.map { |n, l| [n.pluralize(l.count), l.count] }, download: true, colors: ["#3b82f6"] %>
|
|
|
|
<div class="mt-6">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<% top_content_types.each_with_index do |(type, pages), index| %>
|
|
<% break if index >= 10 %>
|
|
<div class="flex items-center p-3 rounded-lg <%= index < 3 ? 'bg-blue-50 border border-blue-100' : '' %>">
|
|
<div class="mr-3 text-xl font-bold <%= index < 3 ? 'text-blue-600' : 'text-gray-400' %>"><%= index + 1 %></div>
|
|
<div>
|
|
<div class="font-medium text-gray-800"><%= type.pluralize(pages.count) %></div>
|
|
<div class="text-sm text-gray-500"><%= pages.count %> <%= 'page'.pluralize(pages.count) %></div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content Creation Timeline -->
|
|
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-8">
|
|
<div class="p-6 border-b border-gray-200">
|
|
<h2 class="text-xl font-semibold text-gray-800 mb-1">Content Creation Timeline</h2>
|
|
<p class="text-gray-600">
|
|
When you created different types of content
|
|
</p>
|
|
</div>
|
|
|
|
<div class="p-6 bg-gray-50">
|
|
<%
|
|
# Get creation timeline data
|
|
timeline_data = {}
|
|
|
|
@content.each do |type, pages|
|
|
next if pages.empty?
|
|
|
|
# Group pages by month manually
|
|
month_groups = pages.group_by { |page| page.created_at.beginning_of_month }
|
|
|
|
month_groups.each do |month_date, month_pages|
|
|
date_str = month_date.strftime("%Y-%m")
|
|
timeline_data[date_str] ||= {}
|
|
timeline_data[date_str][type] = month_pages.count
|
|
end
|
|
end
|
|
|
|
# Convert to format needed for line chart
|
|
chart_data = []
|
|
top_5_types = @content.sort_by { |_, pages| -pages.count }.first(5).map(&:first)
|
|
|
|
top_5_types.each do |type|
|
|
data_points = []
|
|
timeline_data.sort_by { |date, _| date }.each do |date, counts|
|
|
data_points << [date, counts[type] || 0]
|
|
end
|
|
chart_data << {name: type, data: data_points}
|
|
end
|
|
%>
|
|
|
|
<%= line_chart chart_data, download: true %>
|
|
|
|
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div class="bg-gray-100 rounded-lg p-4">
|
|
<h3 class="font-medium text-gray-800 mb-2">First Content Created</h3>
|
|
<%
|
|
first_content = nil
|
|
first_date = nil
|
|
|
|
@content.each do |type, pages|
|
|
next if pages.empty?
|
|
earliest = pages.min_by(&:created_at)
|
|
|
|
if first_date.nil? || earliest.created_at < first_date
|
|
first_content = earliest
|
|
first_date = earliest.created_at
|
|
end
|
|
end
|
|
%>
|
|
|
|
<% if first_content %>
|
|
<div class="text-gray-600">
|
|
Your first page was a <strong><%= first_content.class.name %></strong> named
|
|
"<span class="text-blue-600"><%= first_content.name %></span>"
|
|
created on <%= first_date.strftime("%B %d, %Y") %>
|
|
</div>
|
|
<% else %>
|
|
<div class="text-gray-600">You haven't created any content yet.</div>
|
|
<% end %>
|
|
</div>
|
|
|
|
<div class="bg-gray-100 rounded-lg p-4">
|
|
<h3 class="font-medium text-gray-800 mb-2">Most Recent Content</h3>
|
|
<%
|
|
latest_content = nil
|
|
latest_date = nil
|
|
|
|
@content.each do |type, pages|
|
|
next if pages.empty?
|
|
newest = pages.max_by(&:created_at)
|
|
|
|
if latest_date.nil? || newest.created_at > latest_date
|
|
latest_content = newest
|
|
latest_date = newest.created_at
|
|
end
|
|
end
|
|
%>
|
|
|
|
<% if latest_content %>
|
|
<div class="text-gray-600">
|
|
Your most recent page is a <strong><%= latest_content.class.name %></strong> named
|
|
"<span class="text-blue-600"><%= latest_content.name %></span>"
|
|
created <%= time_ago_in_words(latest_date) %> ago
|
|
</div>
|
|
<% else %>
|
|
<div class="text-gray-600">You haven't created any content yet.</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Link to Achievements Page -->
|
|
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
|
<div class="p-6 flex items-center justify-between">
|
|
<div>
|
|
<h2 class="text-xl font-semibold text-gray-800 mb-1">Achievements & Milestones</h2>
|
|
<p class="text-gray-600">
|
|
Track your progress and unlock achievements as you build your world
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<%= link_to achievements_path, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" do %>
|
|
<i class="material-icons mr-1">emoji_events</i>
|
|
View Achievements
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Handle tab clicks
|
|
document.querySelectorAll('.tab-link').forEach(tabLink => {
|
|
tabLink.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
|
|
const tabId = this.getAttribute('data-tab');
|
|
const tabGroup = this.closest('.border-b').nextElementSibling;
|
|
|
|
// Update tab links
|
|
this.closest('.flex').querySelectorAll('.tab-link').forEach(link => {
|
|
if (link === this) {
|
|
link.classList.add('border-blue-500', 'text-blue-600');
|
|
link.classList.remove('border-transparent', 'text-gray-500');
|
|
} else {
|
|
link.classList.remove('border-blue-500', 'text-blue-600');
|
|
link.classList.add('border-transparent', 'text-gray-500');
|
|
}
|
|
});
|
|
|
|
// Show the selected tab content and hide others
|
|
tabGroup.querySelectorAll('.tab-content').forEach(content => {
|
|
if (content.id === tabId) {
|
|
content.classList.remove('hidden');
|
|
} else {
|
|
content.classList.add('hidden');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
</script>
|