Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dl/public analytics page #44

Merged
merged 9 commits into from
Aug 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
451 changes: 320 additions & 131 deletions README.md

Large diffs are not rendered by default.

35 changes: 24 additions & 11 deletions app/controllers/analytics_controller.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
class AnalyticsController < ApplicationController
before_action :authenticate_user!
before_action :set_user
before_action :check_analytics_visibility
CACHE_EXPIRATION = 30.minutes # You can adjust this or move it to an initializer

def index
@total_page_views = fetch_cached_data("total_page_views") { current_user.page_views.count }
@total_link_clicks = fetch_cached_data("total_link_clicks") { current_user.link_clicks.count }
@total_achievement_views = fetch_cached_data("total_achievement_views") { current_user.achievement_views.count }
@unique_visitors = fetch_cached_data("unique_visitors") { current_user.page_views.select(:ip_address).distinct.count }
@latest_daily_metric = fetch_cached_data("latest_daily_metric") { current_user.daily_metrics.order(date: :desc).first }
@total_page_views = fetch_cached_data("total_page_views") { @user.page_views.count }
@total_link_clicks = fetch_cached_data("total_link_clicks") { @user.link_clicks.count }
@total_achievement_views = fetch_cached_data("total_achievement_views") { @user.achievement_views.count }
@unique_visitors = fetch_cached_data("unique_visitors") { @user.page_views.select(:ip_address).distinct.count }
@latest_daily_metric = fetch_cached_data("latest_daily_metric") { @user.daily_metrics.order(date: :desc).first }
@link_analytics = fetch_cached_data("link_analytics") { fetch_link_analytics }
@achievement_analytics = fetch_cached_data("achievement_analytics") { fetch_achievement_analytics }
@daily_views = fetch_cached_data("daily_views") { fetch_daily_views }
Expand All @@ -15,12 +17,23 @@ def index

private

def set_user
@user = User.find_by!(username: params[:username])
end

def check_analytics_visibility
unless @user == current_user || @user.public_analytics?
flash[:alert] = "This user's analytics are not public."
redirect_to root_path
end
end

def fetch_cached_data(key, &block)
Rails.cache.fetch("#{cache_key_with_version}/#{key}", expires_in: CACHE_EXPIRATION, &block)
end

def fetch_link_analytics
current_user.links.includes(:link_clicks).map do |link|
@user.links.includes(:link_clicks).map do |link|
{
id: link.id,
title: link.title,
Expand All @@ -31,7 +44,7 @@ def fetch_link_analytics
end

def fetch_achievement_analytics
current_user.achievements.includes(:achievement_views).map do |achievement|
@user.achievements.includes(:achievement_views).map do |achievement|
{
id: achievement.id,
title: achievement.title,
Expand All @@ -42,11 +55,11 @@ def fetch_achievement_analytics
end

def fetch_daily_views
current_user.page_views.group_by_day(:visited_at, range: 30.days.ago..Time.now).count
@user.page_views.group_by_day(:visited_at, range: 30.days.ago..Time.now).count
end

def fetch_browser_data
current_user.page_views.group(:browser).count.transform_keys do |user_agent|
@user.page_views.group(:browser).count.transform_keys do |user_agent|
case user_agent
when /Chrome/
'Chrome'
Expand Down Expand Up @@ -85,6 +98,6 @@ def fetch_browser_data
end

def cache_key_with_version
"user_#{current_user.id}_analytics_v1"
"user_#{@user.id}_analytics_v1"
end
end
8 changes: 6 additions & 2 deletions app/controllers/users/registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,12 @@ def sign_up_params
end

def account_update_params
params.require(:user).permit(:email, :password, :password_confirmation, :current_password, :username, :full_name, :tags, :avatar, :banner, :description).tap do |user_params|
user_params[:tags] = user_params[:tags].split(',').map(&:strip).to_json if user_params[:tags].present?
params.require(:user).permit(:email, :password, :password_confirmation, :current_password,
:username, :full_name, :avatar, :banner, :description, :tags,
:public_analytics).tap do |user_params|
if user_params[:tags].present?
user_params[:tags] = user_params[:tags].split(',').map(&:strip).to_json
end
end
end
end
2 changes: 2 additions & 0 deletions app/javascript/entrypoints/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// <%= vite_javascript_tag 'application' %>
import Rails from "@rails/ujs";
import "chartkick/chart.js"
import "flowbite";

Rails.start();

console.log('Vite ⚡️ Rails')
Expand Down
215 changes: 113 additions & 102 deletions app/views/analytics/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
<div class="container mx-auto px-4 py-8 text-white">
<h1 class="text-2xl font-bold mb-6 text-center">Your Analytics Dashboard</h1>
<h1 class="text-2xl font-bold mb-6 text-center flex justify-center items-center space-x-2">
Analytics for <%= @user.username %>
<% if @user.public_analytics? %>
<span class="bg-gray-100 text-black text-sm font-medium px-3 py-1 rounded ml-2">
Public
</span>
<% else %>
<span class="bg-gray-100 text-black text-sm font-medium px-3 py-1 rounded ml-2">
Private
</span>
<% end %>
</h1>
</div>

<!-- Overall Metrics -->
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
<% metrics = [
{ title: "Total Page Views", value: @total_page_views },
{ title: "Total Link Clicks", value: @total_link_clicks },
{ title: "Total Achievement Views", value: @total_achievement_views },
{ title: "Unique Visitors", value: @unique_visitors }
] %>
<% metrics.each do |metric| %>
<div class="bg-gray-800 rounded-lg shadow p-4 flex flex-col justify-between">
<h2 class="text-sm font-semibold text-center mb-2 flex-grow flex items-center justify-center"><%= metric[:title] %></h2>
<p class="text-2xl font-bold text-center text-lime-400"><%= number_with_delimiter(metric[:value]) %></p>
</div>
<% end %>
</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
<% metrics = [
{ title: "Total Page Views", value: @total_page_views },
{ title: "Total Link Clicks", value: @total_link_clicks },
{ title: "Total Achievement Views", value: @total_achievement_views },
{ title: "Unique Visitors", value: @unique_visitors }
] %>
<% metrics.each do |metric| %>
<div class="bg-gray-800 rounded-lg shadow p-4 flex flex-col justify-between">
<h2 class="text-sm font-semibold text-center mb-2 flex-grow flex items-center justify-center"><%= metric[:title] %></h2>
<p class="text-2xl font-bold text-center text-lime-400"><%= number_with_delimiter(metric[:value]) %></p>
</div>
<% end %>
</div>

<!-- Latest Daily Metrics -->
<% if @latest_daily_metric %>
Expand Down Expand Up @@ -43,102 +55,101 @@
<% end %>

<!-- Link Analytics Table -->
<div class="bg-gray-800 rounded-lg shadow p-4 mb-6">
<h2 class="text-xl font-semibold mb-4">Link Analytics</h2>
<p class="text-sm mb-4">Performance data for your links. 'Total Clicks' represents all interactions, while 'Unique Visitors' counts individual users.</p>
<div class="overflow-x-auto">
<table class="w-full text-sm text-center table-auto">
<thead class="text-xs uppercase bg-gray-700">
<tr>
<th scope="col" class="px-4 py-3 rounded-tl-lg max-w-xs break-words">Link</th>
<th scope="col" class="px-4 py-3 w-24">Total Clicks</th>
<th scope="col" class="px-4 py-3 w-24 rounded-tr-lg">Unique Visitors</th>
</tr>
</thead>
<tbody>
<% @link_analytics.each_with_index do |link, index| %>
<tr class="<%= index.even? ? 'bg-gray-800' : 'bg-gray-900' %> border-b border-gray-700">
<th scope="row" class="px-4 py-3 font-medium text-lime-400 break-words max-w-xs">
<%= link[:title] %>
</th>
<td class="px-4 py-3"><%= number_with_delimiter(link[:total_clicks]) %></td>
<td class="px-4 py-3"><%= number_with_delimiter(link[:unique_visitors]) %></td>
<div class="bg-gray-800 rounded-lg shadow p-4 mb-6">
<h2 class="text-xl font-semibold mb-4">Link Analytics</h2>
<p class="text-sm mb-4">Performance data for links. 'Total Clicks' represents all interactions, while 'Unique Visitors' counts individual users.</p>
<div class="overflow-x-auto">
<table class="w-full text-sm text-center table-auto">
<thead class="text-xs uppercase bg-gray-700">
<tr>
<th scope="col" class="px-4 py-3 rounded-tl-lg max-w-xs break-words">Link</th>
<th scope="col" class="px-4 py-3 w-24">Total Clicks</th>
<th scope="col" class="px-4 py-3 w-24 rounded-tr-lg">Unique Visitors</th>
</tr>
<% end %>
</tbody>
</table>
</thead>
<tbody>
<% @link_analytics.each_with_index do |link, index| %>
<tr class="<%= index.even? ? 'bg-gray-800' : 'bg-gray-900' %> border-b border-gray-700">
<th scope="row" class="px-4 py-3 font-medium text-lime-400 break-words max-w-xs">
<%= link[:title] %>
</th>
<td class="px-4 py-3"><%= number_with_delimiter(link[:total_clicks]) %></td>
<td class="px-4 py-3"><%= number_with_delimiter(link[:unique_visitors]) %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>

<!-- Achievement Analytics Table -->
<div class="bg-gray-800 rounded-lg shadow p-4 mb-6">
<h2 class="text-xl font-semibold mb-4">Achievement Analytics</h2>
<p class="text-sm mb-4">Performance data for your achievements. 'Total Views' represents all interactions, while 'Unique Viewers' counts individual users.</p>
<div class="overflow-x-auto">
<table class="w-full text-sm text-center table-auto">
<thead class="text-xs uppercase bg-gray-700">
<tr>
<th scope="col" class="px-4 py-3 rounded-tl-lg max-w-xs break-words">Achievement</th>
<th scope="col" class="px-4 py-3 w-24">Total Views</th>
<th scope="col" class="px-4 py-3 w-24 rounded-tr-lg">Unique Viewers</th>
</tr>
</thead>
<tbody>
<% @achievement_analytics.each_with_index do |achievement, index| %>
<tr class="<%= index.even? ? 'bg-gray-800' : 'bg-gray-900' %> border-b border-gray-700">
<th scope="row" class="px-4 py-3 font-medium text-lime-400 break-words max-w-xs">
<%= achievement[:title] %>
</th>
<td class="px-4 py-3"><%= number_with_delimiter(achievement[:total_views]) %></td>
<td class="px-4 py-3"><%= number_with_delimiter(achievement[:unique_viewers]) %></td>
<!-- Achievement Analytics Table -->
<div class="bg-gray-800 rounded-lg shadow p-4 mb-6">
<h2 class="text-xl font-semibold mb-4">Achievement Analytics</h2>
<p class="text-sm mb-4">Performance data for achievements. 'Total Views' represents all interactions, while 'Unique Viewers' counts individual users.</p>
<div class="overflow-x-auto">
<table class="w-full text-sm text-center table-auto">
<thead class="text-xs uppercase bg-gray-700">
<tr>
<th scope="col" class="px-4 py-3 rounded-tl-lg max-w-xs break-words">Achievement</th>
<th scope="col" class="px-4 py-3 w-24">Total Views</th>
<th scope="col" class="px-4 py-3 w-24 rounded-tr-lg">Unique Viewers</th>
</tr>
<% end %>
</tbody>
</table>
</thead>
<tbody>
<% @achievement_analytics.each_with_index do |achievement, index| %>
<tr class="<%= index.even? ? 'bg-gray-800' : 'bg-gray-900' %> border-b border-gray-700">
<th scope="row" class="px-4 py-3 font-medium text-lime-400 break-words max-w-xs">
<%= achievement[:title] %>
</th>
<td class="px-4 py-3"><%= number_with_delimiter(achievement[:total_views]) %></td>
<td class="px-4 py-3"><%= number_with_delimiter(achievement[:unique_viewers]) %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>

<!-- Charts Section -->
<div class="grid grid-cols-1 gap-6 mb-6">

<!-- Time-based Analytics -->
<div class="bg-gray-800 rounded-lg shadow p-4">
<h2 class="text-xl font-semibold mb-4">Daily Views (Last 30 Days)</h2>
<%= line_chart @daily_views,
colors: ["#84CC16"],
library: {
backgroundColor: 'transparent',
legend: { display: false },
scales: {
x: { ticks: { color: 'white', fontSize: 12 } },
y: { ticks: { color: 'white', fontSize: 12 } },
},
elements: {
line: { tension: 0.4 }, # smooth curves
point: { radius: 3, backgroundColor: 'white' }
<div class="grid grid-cols-1 gap-6 mb-6">
<!-- Time-based Analytics -->
<div class="bg-gray-800 rounded-lg shadow p-4">
<h2 class="text-xl font-semibold mb-4">Daily Views (Last 30 Days)</h2>
<%= line_chart @daily_views,
colors: ["#84CC16"],
library: {
backgroundColor: 'transparent',
legend: { display: false },
scales: {
x: { ticks: { color: 'white', fontSize: 12 } },
y: { ticks: { color: 'white', fontSize: 12 } },
},
elements: {
line: { tension: 0.4 },
point: { radius: 3, backgroundColor: 'white' }
},
title: { display: true, text: 'Daily Views (Last 30 Days)', color: 'white', fontSize: 16 },
responsive: true
},
title: { display: true, text: 'Daily Views (Last 30 Days)', color: 'white', fontSize: 16 },
responsive: true
},
height: "300px" %>
height: "300px" %>
</div>
</div>
</div>

<!-- Browser Usage and Top Sources -->
<div class="grid grid-cols-1 gap-6">
<!-- Browser Usage -->
<div class="bg-gray-800 rounded-lg shadow p-4">
<h2 class="text-xl font-semibold mb-4">Browser Usage</h2>
<%= pie_chart @browser_data,
colors: ["#84CC16", "#22D3EE", "#E879F9", "#F87171", "#A78BFA", "#F59E0B", "#14B8A6", "#3B82F6"],
library: {
backgroundColor: 'transparent',
legend: { position: 'bottom', labels: { color: 'white', fontSize: 12 } },
title: { display: true, text: 'Browser Usage', color: 'white', fontSize: 16 },
responsive: true,
plugins: { datalabels: { color: 'white', font: { weight: 'bold' } } }
},
donut: true,
height: "250px" %>
<div class="grid grid-cols-1 gap-6">
<div class="bg-gray-800 rounded-lg shadow p-4">
<h2 class="text-xl font-semibold mb-4">Browser Usage</h2>
<%= pie_chart @browser_data,
colors: ["#84CC16", "#22D3EE", "#E879F9", "#F87171", "#A78BFA", "#F59E0B", "#14B8A6", "#3B82F6"],
library: {
backgroundColor: 'transparent',
legend: { position: 'bottom', labels: { color: 'white', fontSize: 12 } },
title: { display: true, text: 'Browser Usage', color: 'white', fontSize: 16 },
responsive: true,
plugins: { datalabels: { color: 'white', font: { weight: 'bold' } } }
},
donut: true,
height: "250px" %>
</div>
</div>
</div>
</div>
5 changes: 5 additions & 0 deletions app/views/devise/registrations/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
<%= f.text_area :description, class: 'w-full p-1 rounded text-black' %>
</div>

<div class="field">
<%= f.label :public_analytics %>
<%= f.check_box :public_analytics %>
</div>

<div class="mb-2">
<%= f.label :password, class: 'block text-gray-300 mb-1' %>
<% if @minimum_password_length %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
</button>
<div id="userMenu" class="absolute right-0 mt-2 w-48 bg-gray-700 rounded-md shadow-xl z-20 hidden">
<%= link_to 'Public Page', user_links_path(current_user.username), class: 'block px-4 py-2 text-sm text-white hover:bg-gray-600' %>
<%= link_to 'Analytics', analytics_path, class: 'block px-4 py-2 text-sm text-white hover:bg-gray-600' %>
<%= link_to 'Analytics', user_analytics_path(current_user.username), class: 'block px-4 py-2 text-sm text-white hover:bg-gray-600' %>
<%= link_to 'Links', links_path, class: 'block px-4 py-2 text-sm text-white hover:bg-gray-600' %>
<%= link_to 'Achievements', achievements_path, class: 'block px-4 py-2 text-sm text-white hover:bg-gray-600' %>
<%= link_to 'Profile', edit_user_registration_path, class: 'block px-4 py-2 text-sm text-white hover:bg-gray-600' %>
Expand Down
9 changes: 9 additions & 0 deletions app/views/links/user_links.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
</div>
</div>
</div>

<% if @pinned_links.any? %>
<div class="pinned-links-section mt-4">
<div class="pinned-links flex flex-wrap justify-center">
Expand Down Expand Up @@ -80,4 +81,12 @@
<div class="achievements-section max-w-4xl mx-auto mt-12 text-center">
<p class="text-gray-500">No achievements to display.</p>
</div>
<% end %>

<% if @user.public_analytics %>
<div class="analytics-link text-center mt-8 mb-4">
<%= link_to user_analytics_path(@user.username), class: 'text-gray-400 hover:text-lime-300 text-sm' do %>
<i class="fas fa-chart-line"></i> View Analytics
<% end %>
</div>
<% end %>
2 changes: 1 addition & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
mount Sidekiq::Web => '/sidekiq'

# Other routes
get 'analytics', to: 'analytics#index'
get '/:username/analytics', to: 'analytics#index', as: :user_analytics
get "up" => "rails/health#show", as: :rails_health_check
root to: 'pages#home'
resources :links, only: [:index, :show, :new, :create, :edit, :update, :destroy]
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20240824060759_add_public_analytics_to_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddPublicAnalyticsToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :public_analytics, :boolean, default: false
end
end
Loading
Loading