mirror of
https://github.com/System-End/hackatime.git
synced 2026-04-19 23:32:53 +00:00
Start work on timeline partial
This commit is contained in:
parent
9a2f4ffdeb
commit
40c3147317
4 changed files with 207 additions and 0 deletions
|
|
@ -189,6 +189,20 @@ class StaticPagesController < ApplicationController
|
|||
redirect_to "#{wildcard_host}?auth_key=#{auth_key}", allow_other_host: wildcard_host
|
||||
end
|
||||
|
||||
def timeline
|
||||
render partial: "timeline", locals: {
|
||||
users_to_display: [
|
||||
current_user,
|
||||
User.find(1),
|
||||
User.find(10),
|
||||
User.find(1792),
|
||||
User.find(69),
|
||||
User.find(1476),
|
||||
User.find(805),
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_current_user
|
||||
|
|
|
|||
187
app/views/static_pages/_timeline.html.erb
Normal file
187
app/views/static_pages/_timeline.html.erb
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
<%= turbo_frame_tag "timeline" do %>
|
||||
<%# The controller should pass `users_to_display` (an array of User objects) to this partial %>
|
||||
<%# Example: users_to_display = [current_user, User.find_by(id: 2)] %>
|
||||
<%
|
||||
users_to_display = Array(users_to_display).compact
|
||||
primary_user = users_to_display.first || current_user
|
||||
primary_user_tz = primary_user&.timezone || (current_user&.timezone || 'UTC')
|
||||
%>
|
||||
<%
|
||||
Heartbeat.heartbeat_timeout_duration(10.minutes) unless Rails.env.test?
|
||||
|
||||
timeline_start_hour = 1
|
||||
timeline_end_hour = 23
|
||||
|
||||
user_colors = ['#7C3AED', '#10B981', '#3B82F6', '#F59E0B', '#EF4444', '#DB2777', '#6D28D9']
|
||||
|
||||
num_users = users_to_display.count
|
||||
num_users = 1 if num_users == 0
|
||||
|
||||
line_left_rem = 4.0
|
||||
line_right_rem = 0.5
|
||||
activity_col_area_start_rem = 4.5
|
||||
activity_col_area_end_rem = 0.5
|
||||
gutter_rem = 0.25
|
||||
|
||||
total_gutter_space_rem = num_users > 1 ? (num_users - 1) * gutter_rem : 0
|
||||
css_single_col_width = "calc((100% - #{activity_col_area_start_rem}rem - #{activity_col_area_end_rem}rem - #{total_gutter_space_rem}rem) / #{num_users})"
|
||||
|
||||
pixels_per_hour = 128
|
||||
pixels_per_minute = pixels_per_hour / 60.0
|
||||
%>
|
||||
|
||||
<div style="background-color: #1F2937; color: #FFFFFF; padding: 1rem; border-radius: 0.5rem; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
display: flex; flex-direction: column; height: calc(100vh - 4rem);">
|
||||
<div style="font-size: 1.125rem; line-height: 1.75rem; font-weight: 600; margin-bottom: 1rem; flex-shrink: 0;">
|
||||
<%= Time.current.in_time_zone(primary_user_tz).strftime("%A, %B %-d, %Y") %>
|
||||
</div>
|
||||
|
||||
<div style="position: relative; flex-shrink: 0;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 0.75rem;">
|
||||
<svg style="width: 1.5rem; height: 1.5rem; margin-right: 0.5rem; color: #9CA3AF;" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7"></path></svg>
|
||||
<span style="font-size: 1.25rem; line-height: 1.75rem; font-weight: 500; color: #D1D5DB;">Activity</span>
|
||||
<span style="margin-left: 0.25rem; color: #6B7280; cursor: pointer;" title="Information about activity tracking">?</span>
|
||||
</div>
|
||||
|
||||
<div style="position: absolute; top: 0; right: 0; margin-top: -0.5rem;">
|
||||
<button style="background-color: #374151; color: #D1D5DB; font-size: 0.875rem; line-height: 1.25rem; padding-top: 0.5rem; padding-bottom: 0.5rem; padding-left: 1rem; padding-right: 1rem; border-radius: 0.375rem; border: none; cursor: pointer;">
|
||||
Uncategorized apps o...
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# User ID Header Row - Sticky %>
|
||||
<% if users_to_display.any? %>
|
||||
<div style="display: flex; margin-left: <%= activity_col_area_start_rem %>rem; margin-right: <%= activity_col_area_end_rem %>rem; margin-bottom: 0.5rem;
|
||||
align-items: center; flex-shrink: 0;
|
||||
position: sticky; top: 0; z-index: 20; background-color: #1F2937; padding-bottom: 0.5rem; padding-top: 0.25rem;">
|
||||
<% users_to_display.each_with_index do |user, index| %>
|
||||
<div style="width: <%= css_single_col_width %>;
|
||||
<%= index > 0 ? "margin-left: #{gutter_rem}rem;" : "" %>
|
||||
text-align: center; font-weight: 500; color: #E5E7EB; font-size:0.875rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
||||
title="User ID: <%= user.id %> - <%= user.respond_to?(:username) ? user.username : user.email %>">
|
||||
UID: <%= user.id %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%# Scrollable Timeline Grid Container %>
|
||||
<div id="timeline-grid-scroll-container" style="overflow-y: auto; position: relative; flex-grow: 1;">
|
||||
<%# Generate time slots and hour lines %>
|
||||
<% (timeline_start_hour..timeline_end_hour).each do |hour| %>
|
||||
<div style="display: flex; align-items: center; border-top: 1px solid #374151; padding-bottom: <%= pixels_per_hour / 2.0 %>px; position: relative; box-sizing: border-box; height: <%= pixels_per_hour %>px;">
|
||||
<div style="font-size: 0.75rem; line-height: 1rem; color: #6B7280; width: #{line_left_rem}rem; padding-right: 0.5rem; text-align: right;">
|
||||
<%= Time.utc(2000,1,1, hour).strftime("%-l:00 %p") %>
|
||||
</div>
|
||||
<div style="flex-grow: 1; height: 100%;">
|
||||
<div data-actual-hour="<%= hour %>" style="position: absolute; left: <%= line_left_rem %>rem; right: <%= line_right_rem %>rem; top: 50%; border-bottom: 1px solid #374151; transform: translateY(-50%); z-index: 1;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%# Current Time Indicator Line %>
|
||||
<%
|
||||
current_time_in_zone = Time.current.in_time_zone(primary_user_tz)
|
||||
show_current_time_line = current_time_in_zone.hour >= timeline_start_hour && current_time_in_zone.hour < (timeline_end_hour + 1)
|
||||
|
||||
if show_current_time_line
|
||||
minutes_from_timeline_display_start_for_now = (current_time_in_zone.hour - timeline_start_hour) * 60 + current_time_in_zone.min
|
||||
current_time_line_top_px = (minutes_from_timeline_display_start_for_now * pixels_per_minute)
|
||||
end
|
||||
%>
|
||||
<% if show_current_time_line %>
|
||||
<div style="position: absolute; left: <%= line_left_rem %>rem; right: <%= line_right_rem %>rem; top: <%= current_time_line_top_px %>px; height: 2px; background-color: #F87171; z-index: 15;">
|
||||
<div style="position: absolute; left: -2.5rem; top: -0.4rem; font-size: 0.65rem; color: #F87171; background-color: #1F2937; padding: 0 0.2rem;">NOW</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%# Function to calculate rendering properties for a span (User's extended version) %>
|
||||
<%
|
||||
calculate_span_properties = lambda do |span_data, span_user_tz|
|
||||
return nil unless span_data && span_data[:start_time] && span_data[:duration]
|
||||
|
||||
start_time_in_zone = Time.at(span_data[:start_time]).in_time_zone(span_user_tz)
|
||||
end_time_in_zone = Time.at(span_data[:end_time] || (span_data[:start_time] + span_data[:duration])).in_time_zone(span_user_tz)
|
||||
today_start_of_day_for_span_user = Time.current.in_time_zone(span_user_tz).beginning_of_day
|
||||
|
||||
view_start_datetime = today_start_of_day_for_span_user.advance(hours: timeline_start_hour)
|
||||
view_end_datetime = today_start_of_day_for_span_user.advance(hours: timeline_end_hour + 1)
|
||||
|
||||
effective_start_time = [start_time_in_zone, view_start_datetime].max
|
||||
effective_end_time = [end_time_in_zone, view_end_datetime].min
|
||||
|
||||
return nil if effective_start_time >= effective_end_time
|
||||
|
||||
minutes_from_view_start = ((effective_start_time - view_start_datetime) / 60.0).to_f
|
||||
|
||||
duration_seconds_in_view = effective_end_time - effective_start_time
|
||||
height_px = (duration_seconds_in_view / 60.0) * pixels_per_minute
|
||||
|
||||
return nil if height_px <= 0.5
|
||||
|
||||
final_top_px = (minutes_from_view_start * pixels_per_minute)
|
||||
|
||||
title_parts = []
|
||||
title_parts << "Languages: #{span_data[:languages].join(', ')}" if span_data[:languages]&.any?
|
||||
title_parts << "Projects: #{span_data[:projects_edited].join(', ')}" if span_data[:projects_edited]&.any?
|
||||
title_parts << "Editors: #{span_data[:editors].join(', ')}" if span_data[:editors]&.any?
|
||||
|
||||
files_to_show = span_data[:files_edited] || []
|
||||
if files_to_show.any?
|
||||
max_files_in_tooltip = 5
|
||||
files_display_string = files_to_show.take(max_files_in_tooltip).join(', ')
|
||||
files_display_string += ", ..." if files_to_show.length > max_files_in_tooltip
|
||||
title_parts << "Files: #{files_display_string}"
|
||||
end
|
||||
|
||||
title_parts << "Duration: #{Time.at(span_data[:duration]).utc.strftime('%Hh %Mm %Ss')}"
|
||||
title_parts << "#{start_time_in_zone.strftime("%-l:%M %p")} - #{end_time_in_zone.strftime("%-l:%M %p")}"
|
||||
|
||||
new_title_for_span = title_parts.join("\n")
|
||||
|
||||
{
|
||||
final_top_px: final_top_px.round(2),
|
||||
height_px: height_px.round(2),
|
||||
title: new_title_for_span,
|
||||
display_text_line1: span_data[:projects_edited].present? ? span_data[:projects_edited].join(", ") : "Coding",
|
||||
display_text_line2: span_data[:languages]&.any? ? span_data[:languages].join(", ") : "-",
|
||||
display_text_line3: "#{start_time_in_zone.strftime("%-l:%M %p")} - #{end_time_in_zone.strftime("%-l:%M %p")}",
|
||||
}
|
||||
end
|
||||
%>
|
||||
|
||||
<%# --- Render Activity Blocks for each user (User's extended version) --- %>
|
||||
<% users_to_display.each_with_index do |user, index| %>
|
||||
<%
|
||||
user_spans = user.heartbeats.today.to_span.map do |s|
|
||||
v = s.is_a?(Hash) ? s.symbolize_keys : s
|
||||
v[:files_edited] = user.heartbeats.where('time < ?', v[:end_time]).where('time > ?', v[:start_time]).distinct.pluck(:entity).map { |e| e.split('/').last }
|
||||
v[:projects_edited] = user.heartbeats.where('time < ?', v[:end_time]).where('time > ?', v[:start_time]).distinct.pluck(:project)
|
||||
v[:editors] = user.heartbeats.where('time < ?', v[:end_time]).where('time > ?', v[:start_time]).distinct.pluck(:editor).compact
|
||||
v[:languages] = user.heartbeats.where('time < ?', v[:end_time]).where('time > ?', v[:start_time]).distinct.pluck(:language).compact
|
||||
v
|
||||
end
|
||||
|
||||
current_column_left_offset = "calc(#{activity_col_area_start_rem}rem + (#{index} * (#{css_single_col_width} + #{gutter_rem}rem)))"
|
||||
block_color = user_colors[index % user_colors.length]
|
||||
%>
|
||||
<% user_spans.each do |span_data| %>
|
||||
<% props = calculate_span_properties.call(span_data, user.timezone || primary_user_tz) %>
|
||||
<% next unless props %>
|
||||
<div style="position: absolute; background-color: <%= block_color %>; color: #FFFFFF; border-radius: 0.25rem; font-size: 0.75rem; line-height: 1.1; padding: 2px 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
left: <%= current_column_left_offset %>;
|
||||
width: <%= css_single_col_width %>;
|
||||
top: <%= props[:final_top_px] %>px;
|
||||
height: <%= props[:height_px] %>px;
|
||||
z-index: 10; overflow: hidden; box-sizing: border-box;"
|
||||
title="<%= props[:title] %>">
|
||||
<div style="font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"><%= props[:display_text_line1] %></div>
|
||||
<div style="font-size: 0.7rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"><%= props[:display_text_line2] %></div>
|
||||
<div style="font-size: 0.7rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #E5E7EB;"><%= props[:display_text_line3] %></div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div> <%# End timeline-grid-scroll-container %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -104,6 +104,10 @@
|
|||
<%= turbo_frame_tag "activity_graph", src: activity_graph_static_pages_path do %>
|
||||
<span>Loading...</span>
|
||||
<% end %>
|
||||
|
||||
<%= turbo_frame_tag "timeline", src: timeline_static_pages_path do %>
|
||||
<span>Loading...</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<% if @leaderboard %>
|
||||
<h3>Today's Top Hack Clubbers</h3>
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ Rails.application.routes.draw do
|
|||
get :filterable_dashboard
|
||||
get "🃏", to: "static_pages#🃏", as: :wildcard
|
||||
get :streak
|
||||
|
||||
get :timeline
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue