Skip to content

Commit

Permalink
Merge pull request #44 from loftwah/dl/public-analytics-page
Browse files Browse the repository at this point in the history
Dl/public analytics page
  • Loading branch information
loftwah authored Aug 24, 2024
2 parents 1200c61 + 24b2c9d commit 1f7814a
Show file tree
Hide file tree
Showing 33 changed files with 732 additions and 369 deletions.
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

0 comments on commit 1f7814a

Please sign in to comment.