feat: add recent signup users with avatars (#89)

* feat: add recent signup users with avatars

- Show avatars of recent Hackatime setup users
- Add a hoverable user list showing all setup users with names and photos
- Add tooltips for first 5 users (as preview)

* Add flag to force the 'setup waka' notice

---------

Co-authored-by: Max Wofford <max@maxwofford.com>
This commit is contained in:
ByteAtATime 2025-03-20 20:54:28 -07:00 committed by GitHub
parent 608cd67326
commit 83c2987ab3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 227 additions and 11 deletions

View file

@ -103,7 +103,7 @@
grid-template-columns: 1fr;
gap: 1rem;
}
.video-container {
margin-bottom: 1rem;
}
@ -212,14 +212,20 @@
}
@keyframes flash {
0% { background-color: var(--uchu-yellow); }
100% { background-color: transparent; }
0% {
background-color: var(--uchu-yellow);
}
100% {
background-color: transparent;
}
}
/* Setup tracking button styling */
.setup-notice {
text-align: left;
margin: 2rem 0;
display: flex;
flex-direction: column;
}
.setup-button {
@ -265,15 +271,181 @@
}
}
.setup-info {
display: flex;
align-items: center;
margin-top: 1rem;
flex-wrap: nowrap;
}
.setup-hint {
margin-top: 0.5rem;
margin: 0;
margin-left: 0.5rem;
font-style: italic;
color: var(--muted-color);
animation: bounce 2s ease infinite;
}
.recent-setup-users {
display: flex;
margin: 0;
margin-left: 0;
flex-shrink: 0;
}
.avatar-container {
position: relative;
cursor: pointer;
transition: transform 0.2s ease;
z-index: 1;
margin-left: -15px;
}
.avatar-container:first-child {
margin-left: 0;
}
.avatar-container:hover {
transform: translateY(-3px);
z-index: 5;
}
.setup-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid #e13950;
object-fit: cover;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.more-avatars {
display: flex;
align-items: center;
justify-content: center;
background-color: #e13950;
color: white;
font-weight: bold;
font-size: 14px;
cursor: pointer;
}
.all-users-hover {
position: relative;
}
.users-hover-list {
position: absolute;
display: none;
left: -20px;
top: 45px;
background-color: #333;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 15px;
width: 300px;
z-index: 100;
max-height: 400px;
overflow-y: auto;
}
.users-hover-list h4 {
margin-top: 0;
margin-bottom: 10px;
font-size: 16px;
color: #eee;
border-bottom: 1px solid #aaa;
padding-bottom: 8px;
}
.all-users-hover:hover .users-hover-list {
display: block;
}
.users-hover-list:after {
content: "";
position: absolute;
top: -10px;
left: 30px;
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid #fff;
}
.hover-user-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: none;
overflow-y: visible;
}
.user-item {
display: flex;
align-items: center;
padding: 6px;
border-radius: 6px;
transition: background-color 0.2s;
}
.user-item:hover {
background-color: #f5f5f5;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
margin-right: 10px;
border: 1px solid #e13950;
}
.user-name {
font-weight: 500;
font-size: 14px;
}
.avatar-tooltip {
position: absolute;
top: -35px;
left: 50%;
transform: translateX(-50%);
background-color: #333;
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
z-index: 10;
}
.avatar-tooltip:after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #333 transparent transparent transparent;
}
.avatar-container:hover .avatar-tooltip {
opacity: 1;
visibility: visible;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {

View file

@ -18,7 +18,7 @@ class StaticPagesController < ApplicationController
redirect_to FlavorText.random_time_video.sample, allow_other_host: allowed_hosts
end
@show_wakatime_setup_notice = current_user.heartbeats.empty?
@show_wakatime_setup_notice = current_user.heartbeats.empty? || params[:show_wakatime_setup_notice]
@setup_social_proof = get_setup_social_proof if @show_wakatime_setup_notice
# Get languages and editors in a single query using window functions
@ -176,21 +176,37 @@ class StaticPagesController < ApplicationController
def get_setup_social_proof
# Count users who set up in different time periods
social_proof_for_time_period(5.minutes.ago, 1, "in the last 5 minutes") ||
result = social_proof_for_time_period(5.minutes.ago, 1, "in the last 5 minutes") ||
social_proof_for_time_period(1.hour.ago, 3, "in the last hour") ||
social_proof_for_time_period(1.day.ago, 5, "today") ||
social_proof_for_time_period(1.week.ago, 5, "in the past week") ||
social_proof_for_time_period(1.month.ago, 5, "in the past month") ||
social_proof_for_time_period(Time.current.beginning_of_year, 5, "this year")
result
end
def social_proof_for_time_period(time_period, threshold, humanized_time_period)
count_unique = Heartbeat.where("time > ?", time_period.to_f)
.where(source_type: :test_entry)
.distinct.count(:user_id)
user_ids = Heartbeat.where("time > ?", time_period.to_f)
.where(source_type: :test_entry)
.distinct
.pluck(:user_id)
count_unique = user_ids.count
return nil if count_unique < threshold
all_setup_users = User.where(id: user_ids).flat_map do |user|
{
id: user.id,
avatar_url: user.avatar_url,
display_name: user.display_name || "Hack Clubber"
}
end
@all_setup_users = all_setup_users
@recent_setup_users = all_setup_users.take(5)
"#{count_unique.to_s + ' Hack Clubber'.pluralize(count_unique)} set up Hackatime #{humanized_time_period}"
end
end

View file

@ -33,7 +33,35 @@
<% if @show_wakatime_setup_notice %>
<div class="setup-notice">
<%= link_to "Set up Hackatime! Click me.", my_wakatime_setup_path, class: "auth-button setup-button primary-action" %>
<p class="setup-hint"><%= @setup_social_proof %> (this is real data)</p>
<div class="setup-info">
<% if @recent_setup_users&.any? %>
<div class="recent-setup-users">
<% @recent_setup_users.each do |user| %>
<div class="avatar-container">
<div class="avatar-tooltip"><%= user[:display_name] %></div>
<img src="<%= user[:avatar_url] %>" alt="<%= user[:display_name] %>" class="setup-avatar" />
</div>
<% end %>
<% if @all_setup_users && @all_setup_users.size > 5 %>
<div class="avatar-container all-users-hover" title="See all <%= @all_setup_users.size %> users">
<div class="setup-avatar more-avatars">+<%= @all_setup_users.size - 5 %></div>
<div class="users-hover-list">
<h4>All users who set up Hackatime</h4>
<div class="hover-user-list">
<% @all_setup_users.each do |user| %>
<div class="user-item">
<img src="<%= user[:avatar_url] %>" alt="<%= user[:display_name] %>" class="user-avatar" />
<span class="user-name"><%= user[:display_name] %></span>
</div>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
<% end %>
<p class="setup-hint"><%= @setup_social_proof %> (this is real data)</p>
</div>
</div>
<% end %>