mirror of
https://github.com/System-End/hackatime.git
synced 2026-04-19 19:55:16 +00:00
skeleton profiles (#853)
This commit is contained in:
parent
1ea6b70b98
commit
209b24effa
13 changed files with 203 additions and 80 deletions
|
|
@ -1,7 +1,8 @@
|
|||
class ProfilesController < ApplicationController
|
||||
def show
|
||||
@user = find(params[:username])
|
||||
before_action :find_user
|
||||
before_action :check_profile_visibility, only: %i[time_stats projects languages editors activity]
|
||||
|
||||
def show
|
||||
if @user.nil?
|
||||
render :not_found, status: :not_found, formats: [ :html ]
|
||||
return
|
||||
|
|
@ -9,34 +10,60 @@ class ProfilesController < ApplicationController
|
|||
|
||||
@is_own_profile = current_user && current_user.id == @user.id
|
||||
@profile_visible = @user.allow_public_stats_lookup || @is_own_profile
|
||||
@streak_days = @user.streak_days if @profile_visible
|
||||
end
|
||||
|
||||
return unless @profile_visible
|
||||
def time_stats
|
||||
Time.use_zone(@user.timezone) do
|
||||
stats = ProfileStatsService.new(@user).stats
|
||||
render partial: "profiles/time_stats", locals: {
|
||||
total_time_today: stats[:total_time_today],
|
||||
total_time_week: stats[:total_time_week],
|
||||
total_time_all: stats[:total_time_all]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
load
|
||||
def projects
|
||||
Time.use_zone(@user.timezone) do
|
||||
stats = ProfileStatsService.new(@user).stats
|
||||
render partial: "profiles/projects", locals: { projects: stats[:top_projects_month] }
|
||||
end
|
||||
end
|
||||
|
||||
def languages
|
||||
Time.use_zone(@user.timezone) do
|
||||
stats = ProfileStatsService.new(@user).stats
|
||||
render partial: "profiles/languages", locals: { languages: stats[:top_languages] }
|
||||
end
|
||||
end
|
||||
|
||||
def editors
|
||||
Time.use_zone(@user.timezone) do
|
||||
stats = ProfileStatsService.new(@user).stats
|
||||
render partial: "profiles/editors", locals: { editors: stats[:top_editors] }
|
||||
end
|
||||
end
|
||||
|
||||
def activity
|
||||
Time.use_zone(@user.timezone) do
|
||||
daily_durations = @user.heartbeats.daily_durations(user_timezone: @user.timezone).to_h
|
||||
render partial: "profiles/activity", locals: { daily_durations: daily_durations, user_tz: @user.timezone }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find(username)
|
||||
User.find_by(username: username)
|
||||
def find_user
|
||||
@user = User.find_by(username: params[:username])
|
||||
end
|
||||
|
||||
def load
|
||||
Time.use_zone(@user.timezone) do
|
||||
stats = ProfileStatsService.new(@user).stats
|
||||
def check_profile_visibility
|
||||
return if @user.nil?
|
||||
|
||||
@total_time_today = stats[:total_time_today]
|
||||
@total_time_week = stats[:total_time_week]
|
||||
@total_time_all = stats[:total_time_all]
|
||||
@top_languages = stats[:top_languages]
|
||||
@top_projects = stats[:top_projects]
|
||||
@top_projects_month = stats[:top_projects_month]
|
||||
@top_editors = stats[:top_editors]
|
||||
is_own_profile = current_user && current_user.id == @user.id
|
||||
profile_visible = @user.allow_public_stats_lookup || is_own_profile
|
||||
|
||||
@daily_durations = @user.heartbeats.daily_durations(user_timezone: @user.timezone).to_h
|
||||
|
||||
@streak_days = @user.streak_days
|
||||
@cool = @user.trust_level == 2
|
||||
end
|
||||
head :not_found unless profile_visible
|
||||
end
|
||||
end
|
||||
|
|
|
|||
8
app/views/profiles/_activity.html.erb
Normal file
8
app/views/profiles/_activity.html.erb
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<%= turbo_frame_tag "profile_activity" do %>
|
||||
<% if daily_durations.present? %>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Activity</h2>
|
||||
<%= render "static_pages/activity_graph", daily_durations: daily_durations, length_of_busiest_day: 8.hours.to_i, user_tz: user_tz %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
11
app/views/profiles/_activity_skeleton.html.erb
Normal file
11
app/views/profiles/_activity_skeleton.html.erb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<div class="animate-pulse">
|
||||
<h2 class="text-xl font-bold mb-4">Activity</h2>
|
||||
<div class="w-full overflow-x-auto pb-2.5">
|
||||
<div class="grid grid-rows-7 grid-flow-col gap-1 w-full lg:w-1/2">
|
||||
<% 364.times do %>
|
||||
<div class="w-3 h-3 bg-[#151b23] animate-pulse rounded-sm"></div>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="super invisible">Calculated in UTC</p>
|
||||
</div>
|
||||
</div>
|
||||
20
app/views/profiles/_editors.html.erb
Normal file
20
app/views/profiles/_editors.html.erb
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<%= turbo_frame_tag "profile_editors" do %>
|
||||
<% if editors.present? && editors.size > 0 %>
|
||||
<div class="border border-primary rounded-xl p-5">
|
||||
<h2 class="text-xl font-bold mb-4">Favorite Editors</h2>
|
||||
<div class="flex flex-col gap-3">
|
||||
<% max_duration = editors.values.map(&:to_f).select { |d| d.finite? && d > 0 }.max || 1 %>
|
||||
<% editors.each do |editor, duration| %>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-24 text-sm text-gray-300 truncate"><%= editor %></div>
|
||||
<div class="flex-1 h-6 bg-darkless rounded-full overflow-hidden relative">
|
||||
<div class="h-full bg-primary rounded-full flex items-center justify-end pr-2" style="width: <%= [(duration.to_f.finite? ? duration.to_f : 0) / max_duration * 100, 100].min.round %>%">
|
||||
<span class="text-xs font-medium text-white"><%= ApplicationController.helpers.short_time_simple(duration) %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
11
app/views/profiles/_editors_skeleton.html.erb
Normal file
11
app/views/profiles/_editors_skeleton.html.erb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<div class="border border-primary rounded-xl p-5 animate-pulse">
|
||||
<h2 class="text-xl font-bold mb-4 invisible">Favorite Editors</h2>
|
||||
<div class="flex flex-col gap-3">
|
||||
<% 5.times do |i| %>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-24 h-4 bg-darkless rounded"></div>
|
||||
<div class="flex-1 h-6 bg-darkless rounded-full overflow-hidden relative"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
20
app/views/profiles/_languages.html.erb
Normal file
20
app/views/profiles/_languages.html.erb
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<%= turbo_frame_tag "profile_languages" do %>
|
||||
<% if languages.present? && languages.size > 0 %>
|
||||
<div class="border border-primary rounded-xl p-5">
|
||||
<h2 class="text-xl font-bold mb-4">Top Languages</h2>
|
||||
<div class="flex flex-col gap-3">
|
||||
<% max_duration = languages.values.map(&:to_f).select { |d| d.finite? && d > 0 }.max || 1 %>
|
||||
<% languages.each do |language, duration| %>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-24 text-sm text-gray-300 truncate"><%= ApplicationController.helpers.display_language_name(language) || "Unknown" %></div>
|
||||
<div class="flex-1 h-6 bg-darkless rounded-full overflow-hidden relative">
|
||||
<div class="h-full bg-primary rounded-full flex items-center justify-end pr-2" style="width: <%= [(duration.to_f.finite? ? duration.to_f : 0) / max_duration * 100, 100].min.round %>%">
|
||||
<span class="text-xs font-medium text-white"><%= ApplicationController.helpers.short_time_simple(duration) %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
11
app/views/profiles/_languages_skeleton.html.erb
Normal file
11
app/views/profiles/_languages_skeleton.html.erb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<div class="border border-primary rounded-xl p-5 animate-pulse">
|
||||
<h2 class="text-xl font-bold mb-4 invisible">Top Languages</h2>
|
||||
<div class="flex flex-col gap-3">
|
||||
<% 5.times do |i| %>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-24 h-4 bg-darkless rounded"></div>
|
||||
<div class="flex-1 h-6 bg-darkless rounded-full overflow-hidden relative"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
8
app/views/profiles/_projects.html.erb
Normal file
8
app/views/profiles/_projects.html.erb
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<%= turbo_frame_tag "profile_projects" do %>
|
||||
<% if projects.present? && projects.size > 0 %>
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-bold mb-4">Top Projects <span class="text-gray-400 font-normal text-base">(Past Month)</span></h2>
|
||||
<%= render "profiles/project_cards", projects: projects %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
20
app/views/profiles/_projects_skeleton.html.erb
Normal file
20
app/views/profiles/_projects_skeleton.html.erb
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<div class="mb-6 animate-pulse">
|
||||
<h2 class="text-xl font-bold mb-4 invisible">Top Projects <span class="text-gray-400 font-normal text-base">(Past Month)</span></h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<% 6.times do %>
|
||||
<div class="border border-primary rounded-xl p-5 flex flex-col gap-3">
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="flex-1 relative">
|
||||
<h3 class="text-lg font-semibold invisible">Placeholder</h3>
|
||||
<div class="absolute inset-y-0 left-0 w-32 bg-darkless rounded my-auto h-5"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 relative">
|
||||
<span class="text-2xl font-bold invisible">00h 00m</span>
|
||||
<div class="absolute inset-y-0 left-0 w-20 bg-darkless rounded my-auto h-6"></div>
|
||||
</div>
|
||||
<div class="w-full h-2 bg-darkless rounded-full"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
16
app/views/profiles/_time_stats.html.erb
Normal file
16
app/views/profiles/_time_stats.html.erb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<%= turbo_frame_tag "profile_time_stats" do %>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||
<div class="border border-primary rounded-xl p-5 text-center">
|
||||
<div class="text-sm text-gray-400 uppercase tracking-wide mb-1">Today</div>
|
||||
<div class="text-2xl font-bold text-primary"><%= ApplicationController.helpers.short_time_simple(total_time_today) %></div>
|
||||
</div>
|
||||
<div class="border border-primary rounded-xl p-5 text-center">
|
||||
<div class="text-sm text-gray-400 uppercase tracking-wide mb-1">This Week</div>
|
||||
<div class="text-2xl font-bold text-primary"><%= ApplicationController.helpers.short_time_simple(total_time_week) %></div>
|
||||
</div>
|
||||
<div class="border border-primary rounded-xl p-5 text-center">
|
||||
<div class="text-sm text-gray-400 uppercase tracking-wide mb-1">All Time</div>
|
||||
<div class="text-2xl font-bold text-primary"><%= ApplicationController.helpers.short_time_simple(total_time_all) %></div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
14
app/views/profiles/_time_stats_skeleton.html.erb
Normal file
14
app/views/profiles/_time_stats_skeleton.html.erb
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6 animate-pulse">
|
||||
<div class="border border-primary rounded-xl p-5 text-center">
|
||||
<div class="text-sm text-gray-400 uppercase tracking-wide mb-1 invisible">Today</div>
|
||||
<div class="h-8 w-20 bg-darkless rounded mx-auto"></div>
|
||||
</div>
|
||||
<div class="border border-primary rounded-xl p-5 text-center">
|
||||
<div class="text-sm text-gray-400 uppercase tracking-wide mb-1 invisible">This Week</div>
|
||||
<div class="h-8 w-24 bg-darkless rounded mx-auto"></div>
|
||||
</div>
|
||||
<div class="border border-primary rounded-xl p-5 text-center">
|
||||
<div class="text-sm text-gray-400 uppercase tracking-wide mb-1 invisible">All Time</div>
|
||||
<div class="h-8 w-28 bg-darkless rounded mx-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -24,74 +24,26 @@
|
|||
</div>
|
||||
|
||||
<% if @profile_visible %>
|
||||
<%= turbo_frame_tag "profile_time_stats", src: profile_time_stats_path(@user.username), loading: :lazy do %>
|
||||
<%= render "profiles/time_stats_skeleton" %>
|
||||
<% end %>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||
<div class="border border-primary rounded-xl p-5 text-center">
|
||||
<div class="text-sm text-gray-400 uppercase tracking-wide mb-1">Today</div>
|
||||
<div class="text-2xl font-bold text-primary"><%= ApplicationController.helpers.short_time_simple(@total_time_today) %></div>
|
||||
</div>
|
||||
<div class="border border-primary rounded-xl p-5 text-center">
|
||||
<div class="text-sm text-gray-400 uppercase tracking-wide mb-1">This Week</div>
|
||||
<div class="text-2xl font-bold text-primary"><%= ApplicationController.helpers.short_time_simple(@total_time_week) %></div>
|
||||
</div>
|
||||
<div class="border border-primary rounded-xl p-5 text-center">
|
||||
<div class="text-sm text-gray-400 uppercase tracking-wide mb-1">All Time</div>
|
||||
<div class="text-2xl font-bold text-primary"><%= ApplicationController.helpers.short_time_simple(@total_time_all) %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @top_projects_month.present? && @top_projects_month.size > 0 %>
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-bold mb-4">Top Projects <span class="text-gray-400 font-normal text-base">(Past Month)</span></h2>
|
||||
<%= render "profiles/project_cards", projects: @top_projects_month %>
|
||||
</div>
|
||||
<%= turbo_frame_tag "profile_projects", src: profile_projects_path(@user.username), loading: :lazy do %>
|
||||
<%= render "profiles/projects_skeleton" %>
|
||||
<% end %>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<% if @top_languages.present? && @top_languages.size > 0 %>
|
||||
<div class="border border-primary rounded-xl p-5">
|
||||
<h2 class="text-xl font-bold mb-4">Top Languages</h2>
|
||||
<div class="flex flex-col gap-3">
|
||||
<% max_duration = @top_languages.values.map(&:to_f).select { |d| d.finite? && d > 0 }.max || 1 %>
|
||||
<% @top_languages.each do |language, duration| %>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-24 text-sm text-gray-300 truncate"><%= ApplicationController.helpers.display_language_name(language) || "Unknown" %></div>
|
||||
<div class="flex-1 h-6 bg-darkless rounded-full overflow-hidden relative">
|
||||
<div class="h-full bg-primary rounded-full flex items-center justify-end pr-2" style="width: <%= [(duration.to_f.finite? ? duration.to_f : 0) / max_duration * 100, 100].min.round %>%">
|
||||
<span class="text-xs font-medium text-white"><%= ApplicationController.helpers.short_time_simple(duration) %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<%= turbo_frame_tag "profile_languages", src: profile_languages_path(@user.username), loading: :lazy do %>
|
||||
<%= render "profiles/languages_skeleton" %>
|
||||
<% end %>
|
||||
|
||||
<% if @top_editors.present? && @top_editors.size > 0 %>
|
||||
<div class="border border-primary rounded-xl p-5">
|
||||
<h2 class="text-xl font-bold mb-4">Favorite Editors</h2>
|
||||
<div class="flex flex-col gap-3">
|
||||
<% max_duration = @top_editors.values.map(&:to_f).select { |d| d.finite? && d > 0 }.max || 1 %>
|
||||
<% @top_editors.each do |editor, duration| %>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-24 text-sm text-gray-300 truncate"><%= editor %></div>
|
||||
<div class="flex-1 h-6 bg-darkless rounded-full overflow-hidden relative">
|
||||
<div class="h-full bg-primary rounded-full flex items-center justify-end pr-2" style="width: <%= [(duration.to_f.finite? ? duration.to_f : 0) / max_duration * 100, 100].min.round %>%">
|
||||
<span class="text-xs font-medium text-white"><%= ApplicationController.helpers.short_time_simple(duration) %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<%= turbo_frame_tag "profile_editors", src: profile_editors_path(@user.username), loading: :lazy do %>
|
||||
<%= render "profiles/editors_skeleton" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if @daily_durations.present? %>
|
||||
<div class="mt-6">
|
||||
<h2 class="text-xl font-bold mb-4">Activity</h2>
|
||||
<%= render "static_pages/activity_graph", daily_durations: @daily_durations, length_of_busiest_day: 8.hours.to_i, user_tz: @user.timezone %>
|
||||
</div>
|
||||
<%= turbo_frame_tag "profile_activity", src: profile_activity_path(@user.username), loading: :lazy do %>
|
||||
<%= render "profiles/activity_skeleton" %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="text-center py-16">
|
||||
|
|
|
|||
|
|
@ -275,6 +275,11 @@ Rails.application.routes.draw do
|
|||
end
|
||||
|
||||
get "/@:username", to: "profiles#show", as: :profile, constraints: { username: /[A-Za-z0-9_-]+/ }
|
||||
get "/@:username/time_stats", to: "profiles#time_stats", as: :profile_time_stats, constraints: { username: /[A-Za-z0-9_-]+/ }
|
||||
get "/@:username/projects", to: "profiles#projects", as: :profile_projects, constraints: { username: /[A-Za-z0-9_-]+/ }
|
||||
get "/@:username/languages", to: "profiles#languages", as: :profile_languages, constraints: { username: /[A-Za-z0-9_-]+/ }
|
||||
get "/@:username/editors", to: "profiles#editors", as: :profile_editors, constraints: { username: /[A-Za-z0-9_-]+/ }
|
||||
get "/@:username/activity", to: "profiles#activity", as: :profile_activity, constraints: { username: /[A-Za-z0-9_-]+/ }
|
||||
|
||||
# SEO routes
|
||||
get "/sitemap.xml", to: "sitemap#sitemap", defaults: { format: "xml" }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue