remove scrapyard leaderboards

This commit is contained in:
Echo 2025-06-27 10:29:56 -04:00
parent 509f763e13
commit 9b4f764c33
No known key found for this signature in database
10 changed files with 0 additions and 589 deletions

View file

@ -1,18 +0,0 @@
.scrapyard-notice {
background-color: rgba(var(--uchu-green-rgb), 0.2);
border-radius: 5px;
border: 1px solid var(--uchu-green);
color: var(--uchu-yellow) !important;
position: relative;
padding: 1rem;
margin-top: 3rem;
max-width: 26em;
}
.scrapyard-notice img {
position: absolute;
max-width: 100px;
max-height: 100px;
top: -50px;
right: 20px;
}

View file

@ -1,244 +0,0 @@
/* Common styles */
.scrapyard-leaderboard,
.scrapyard-event {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.scrapyard-leaderboard h1,
.scrapyard-event h1 {
margin-top: 0;
}
/* Index page styles */
.sort-controls {
margin-bottom: 20px;
text-align: left;
}
.sort-link {
color: inherit;
text-decoration: none;
opacity: 0.7;
transition: opacity 0.2s;
padding: 4px 8px;
}
.sort-link:hover {
opacity: 1;
}
.sort-link.active {
opacity: 1;
font-weight: bold;
}
.event-card {
background: var(--uchu-light-green);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
position: relative;
transition: all 0.3s;
border: 2px solid transparent;
}
.event-card.highlighted {
border: 2px solid #FFE066;
background: #f4f9f4;
box-shadow: 0 0 10px rgba(255, 224, 102, 0.3);
}
.pin-link {
position: absolute;
top: 12px;
right: 12px;
text-decoration: none;
opacity: 0.5;
transition: opacity 0.2s;
}
.pin-link:hover {
opacity: 1;
}
.event-card h2 {
margin: 0 0 15px 0;
}
.event-card h2 a {
color: inherit;
text-decoration: none;
}
.event-card h2 a:hover {
text-decoration: underline;
}
.ordinal {
font-size: 0.9em;
opacity: 0.7;
font-weight: normal;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.stat {
display: flex;
flex-direction: column;
}
.stat label {
font-size: 0.9em;
opacity: 0.8;
}
.stat span {
font-size: 1.2em;
font-weight: bold;
}
.stat .average {
font-size: 0.9em;
opacity: 0.8;
font-weight: normal;
margin-left: 4px;
}
.stat .no-time {
opacity: 0.7;
font-weight: normal;
}
.active-sort {
font-weight: bold !important;
opacity: 1 !important;
}
/* Show page styles */
.header {
margin-bottom: 30px;
}
.header h1 {
margin: 0 0 10px 0;
}
.back-link {
color: inherit;
text-decoration: none;
opacity: 0.7;
}
.back-link:hover {
opacity: 1;
}
.attendee-leaderboard {
background: var(--uchu-light-green);
border-radius: 8px;
padding: 20px;
}
.attendee-leaderboard h2 {
margin: 0 0 20px 0;
}
.attendees {
display: flex;
flex-direction: column;
gap: 10px;
}
.attendee-card {
display: flex;
align-items: center;
gap: 15px;
padding: 10px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.rank {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.1);
border-radius: 50%;
font-weight: bold;
}
.info {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.name {
font-weight: bold;
}
.time {
opacity: 0.8;
}
.no-data {
text-align: center;
padding: 20px;
opacity: 0.7;
}
.event-card {
background: rgba(var(--uchu-green-rgb), 0.1);
border-color: rgba(var(--uchu-green-rgb), 0.2);
}
.event-card.highlighted {
background: rgba(var(--uchu-green-rgb), 0.15);
border-color: var(--uchu-yellow);
box-shadow: 0 0 10px rgba(var(--uchu-yellow-rgb), 0.2);
}
.event-card h2 a:hover {
color: var(--uchu-yellow);
}
.attendee-leaderboard {
background: rgba(var(--uchu-green-rgb), 0.1);
border: 1px solid rgba(var(--uchu-green-rgb), 0.2);
}
.attendee-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.stat label,
.time,
.ordinal {
color: rgba(255, 255, 255, 0.7);
}
.stat .average,
.stat .no-time {
color: rgba(255, 255, 255, 0.6);
}
.back-link,
.sort-link {
color: rgba(255, 255, 255, 0.7);
}
.back-link:hover,
.sort-link:hover,
.sort-link.active {
color: var(--uchu-yellow);
}

View file

@ -1,138 +0,0 @@
class ScrapyardLeaderboardsController < ApplicationController
# March 14, 2024 at 8:00 PM Eastern
TRACKING_START_TIME = Time.find_zone("Eastern Time (US & Canada)").local(2025, 3, 14, 20, 0).to_i
TRACKING_END_TIME = Time.find_zone("Eastern Time (US & Canada)").local(2025, 3, 17, 20, 0).to_i
PINNED_EVENT_TIMEOUT = 1.minutes
helper_method :is_watched?
def index
@sort_by = params[:sort] == "average" ? "average" : "total"
# If there's a pinned event, cache it
if params[:event_pin].present?
mark_event_as_pinned(params[:event_pin])
end
# Cache the expensive computations for 10 seconds
@event_stats = Rails.cache.fetch("scrapyard_leaderboard_stats", expires_in: 10.seconds) do
# Get all attendees and their emails in one query
event_attendees = Warehouse::ScrapyardLocalAttendee
.where.not(email: nil)
.group_by(&:event_id)
# Only get events that have attendees
events = Warehouse::ScrapyardEvent
.where(id: event_attendees.keys)
.order(created_at: :desc)
# Pre-fetch all users for the emails we have
all_emails = event_attendees.values.flatten.map(&:email).compact
email_to_user = EmailAddress.where(email: all_emails).includes(:user).index_by(&:email)
# For each event, get the total coding time of its attendees
event_stats = events.map do |event|
# Get attendees for this event
attendees = event_attendees[event.id] || []
users = attendees
.map { |a| email_to_user[a.email]&.user }
.compact
.uniq
# Calculate total duration for all users in one query
total_seconds = if users.any?
Heartbeat.where(user: users)
.where("time >= ?", TRACKING_START_TIME)
.where("time <= ?", TRACKING_END_TIME)
.group(:user_id)
.duration_seconds
.values
.sum
else
0
end
{
event: event,
total_seconds: total_seconds,
hackatime_users: users.count,
total_attendees: attendees.count,
average_seconds_per_attendee: users.any? ? (total_seconds.to_f / users.count) : 0
}
end
# filter out events with no users
event_stats.select { |stats| stats[:hackatime_users] > 0 }
end
# Sort by selected metric (do this outside cache since it depends on params)
@event_stats = @event_stats.sort_by do |stats|
if @sort_by == "average"
-stats[:average_seconds_per_attendee]
else
-stats[:total_seconds]
end
end
end
def pin
event_id = params[:id]
mark_event_as_pinned(event_id)
head :ok
end
def show
@event = Warehouse::ScrapyardEvent.find(params[:id])
@attendee_stats = Rails.cache.fetch("scrapyard_leaderboard_event_#{@event.id}", expires_in: 10.seconds) do
attendees = Warehouse::ScrapyardLocalAttendee
.where.not(email: nil)
.for_event(@event)
.uniq { |attendee| attendee.email }
emails = attendees.map(&:email).compact
email_to_user = EmailAddress.where(email: emails).includes(:user).index_by(&:email)
user_attendees = attendees.map do |attendee|
{
user: email_to_user[attendee.email]&.user,
attendee: attendee
}
end.select { |ua| ua[:user].present? }
users = user_attendees.map { |ua| ua[:user] }
user_heartbeats = Heartbeat
.where(user: users)
.where("time >= ?", TRACKING_START_TIME)
.where("time <= ?", TRACKING_END_TIME)
.group(:user_id)
.duration_seconds
stats = user_attendees.map do |ua|
email = ua[:attendee].email if Rails.env.development?
{
user: ua[:user],
display_name: ua[:user].username || ua[:attendee].preferred_name || "Anonymous",
total_seconds: user_heartbeats[ua[:user].id] || 0,
email: email || nil
}
end
stats.sort_by { |stats| -stats[:total_seconds] }
end
end
private
def mark_event_as_pinned(event_id)
Rails.cache.write(
"pinned_event:#{event_id}",
true,
expires_in: PINNED_EVENT_TIMEOUT
)
end
def is_watched?(event_id)
Rails.cache.exist?("pinned_event:#{event_id}")
end
end

View file

@ -1,9 +0,0 @@
class Warehouse::ScrapyardEvent < WarehouseRecord
self.table_name = "airtable_hack_club_scrapyard_appigkif7gbvisalg.events"
# Prevent these columns from messing with acriverecord
self.ignored_columns += [ "errors" ]
# local attendees is a list of airtable ids
has_many :local_attendees, class_name: "Warehouse::ScrapyardLocalAttendee", foreign_key: "event_id"
end

View file

@ -1,25 +0,0 @@
class Warehouse::ScrapyardLocalAttendee < WarehouseRecord
self.table_name = "airtable_hack_club_scrapyard_appigkif7gbvisalg.local_attendees"
# The event field in Airtable is a JSONB array with a single event ID
def event_id
self[:event]&.first
end
# Override the event association to handle the array field
def event
return nil if event_id.nil?
Warehouse::ScrapyardEvent.find_by(id: event_id)
end
# Find the associated user through their email
def user
return nil if self[:email].blank?
EmailAddress.find_by(email: self[:email])&.user
end
# Scope to find attendees for a specific event
scope :for_event, ->(event) {
where("event ? :event_id", event_id: event.id)
}
end

View file

@ -1,115 +0,0 @@
<div class="scrapyard-leaderboard">
<h1>Scrapyard Leaderboard</h1>
<div class="sort-controls">
Sort by:
<%= link_to "Total Time", scrapyard_leaderboards_path(sort: 'total'),
class: "sort-link #{@sort_by == 'total' ? 'active' : ''}" %>
|
<%= link_to "Average Time", scrapyard_leaderboards_path(sort: 'average'),
class: "sort-link #{@sort_by == 'average' ? 'active' : ''}" %>
</div>
<% @event_stats.each_with_index do |stats, index| %>
<div class="event-card" id="event-<%= stats[:event].id %>">
<h2>
<%= link_to scrapyard_leaderboard_path(stats[:event]) do %>
<% case index %>
<% when 0 %>
🥇
<% when 1 %>
🥈
<% when 2 %>
🥉
<% else %>
<span class="ordinal"><%= index + 1 %></span>
<% end %>
<%= stats[:event].name %>
<% if is_watched?(stats[:event].id) %>
<span title="Currently being watched">👀</span>
<% end %>
<% end %>
</h2>
<div class="stats">
<div class="stat">
<label>Coding time</label>
<span>
<% if stats[:total_seconds] > 0 %>
<span class="<%= @sort_by == 'total' ? 'active-sort' : '' %>">
<%= short_time_detailed(stats[:total_seconds]) %>
</span>
<span class="average <%= @sort_by == 'average' ? 'active-sort' : '' %>">
(<%= short_time_detailed(stats[:average_seconds_per_attendee]) %> avg)
</span>
<% else %>
<span class="no-time">No time logged</span>
<% end %>
</span>
</div>
<div class="stat">
<label>Hackatime users / Estimated attendees</label>
<span>
<%= stats[:hackatime_users] %>/<%= stats[:total_attendees] %>
<% percent = (stats[:hackatime_users].to_f / stats[:total_attendees] * 100) %>
(<%= percent < 1 ? "%.1f" % percent : percent.to_i %>%)
</span>
</div>
</div>
</div>
<% end %>
</div>
<% content_for :head do %>
<script>
function handlePin() {
const params = new URLSearchParams(window.location.search);
const pinId = params.get('event_pin');
if (pinId) {
const targetCard = document.getElementById('event-' + pinId);
if (targetCard) {
// Remove any existing highlights
document.querySelectorAll('.event-card.highlighted').forEach(card => {
card.classList.remove('highlighted');
});
targetCard.classList.add('highlighted');
// Center the card
const windowHeight = window.innerHeight;
const cardHeight = targetCard.offsetHeight;
const cardTop = targetCard.offsetTop;
const scrollTo = cardTop - (windowHeight - cardHeight) / 2;
window.scrollTo({ top: scrollTo });
}
}
}
// Handle both initial page load and subsequent Turbo navigations
document.addEventListener('turbo:load', handlePin);
document.addEventListener('turbo:render', handlePin);
document.addEventListener('DOMContentLoaded', handlePin);
// Set up auto-refresh if we have a pin
document.addEventListener('turbo:load', function() {
const params = new URLSearchParams(window.location.search);
if (params.has('event_pin')) {
// Clear any existing refresh interval
if (window.refreshInterval) {
clearInterval(window.refreshInterval);
}
// Set up new refresh using Turbo
window.refreshInterval = setInterval(() => {
Turbo.visit(window.location.href, { action: 'replace' });
}, 5000);
}
});
// Clean up interval when leaving the page
document.addEventListener('turbo:before-visit', function() {
if (window.refreshInterval) {
clearInterval(window.refreshInterval);
}
});
</script>
<% end %>

View file

@ -1,32 +0,0 @@
<div class="scrapyard-event">
<div class="header">
<h1><%= @event.name %></h1>
<%= link_to "← Back to all scrapyards", scrapyard_leaderboards_path, class: "back-link" %>
</div>
<div class="attendee-leaderboard">
<h2>Hackatime Users</h2>
<% if @attendee_stats.any? %>
<div class="attendees">
<% @attendee_stats.each_with_index do |stats, index| %>
<div class="attendee-card">
<div class="info">
<div class="name">
<%= stats[:display_name] %>
</div>
<div class="time">
<%= ApplicationController.helpers.short_time_simple(stats[:total_seconds]) %>
</div>
</div>
</div>
<% end %>
</div>
<% else %>
<p class="no-data">No coding activity recorded for this event yet.</p>
<% end %>
</div>
</div>
<% content_for :head do %>
<% end %>

View file

@ -1,5 +0,0 @@
<div class="scrapyard-notice">
<img src="https://hc-cdn.hel1.your-objectstorage.com/s/v3/936b246fe5593e09ceed33d04132212223241e71_hourglass.png">
<h2>Went to Scrapyard?</h2>
<p>The event is over, but you can still see the <%= link_to "leaderboards", scrapyard_leaderboards_path %>!</p>
</div>

View file

@ -159,8 +159,6 @@ Rails.application.routes.draw do
end
end
resources :scrapyard_leaderboards, only: [ :index, :show ]
# SEO routes
get "/sitemap.xml", to: "sitemap#sitemap", defaults: { format: "xml" }
end

View file

@ -5,7 +5,6 @@ ignored_endpoints:
- SessionsController#impersonate
- SessionsController#stop_impersonating
- LetterOpenerWeb::Engine
- ScrapyardLeaderboardsController#index
- Api::Hackatime::V1::HackatimeController#push_heartbeats
- Api::Hackatime::V1::HackatimeController#status_bar_today
- Api::V1::StatsController#user_stats