skeleton profiles (#853)

This commit is contained in:
Echo 2026-01-26 00:08:33 -05:00 committed by GitHub
parent 1ea6b70b98
commit 209b24effa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 203 additions and 80 deletions

View file

@ -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

View 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 %>

View 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>

View 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 %>

View 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>

View 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 %>

View 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>

View 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 %>

View 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>

View 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 %>

View 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>

View file

@ -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">

View file

@ -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" }