From 72b340f12819440b41f3098ad7fa0a76831acb46 Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 26 Jun 2025 14:13:58 -0400 Subject: [PATCH 1/4] make fonts work --- app/assets/stylesheets/application.css | 12 ++++++------ {app/assets => public}/fonts/Bold.woff | Bin {app/assets => public}/fonts/Bold.woff2 | Bin {app/assets => public}/fonts/Italic.woff | Bin {app/assets => public}/fonts/Italic.woff2 | Bin {app/assets => public}/fonts/Regular.woff | Bin {app/assets => public}/fonts/Regular.woff2 | Bin 7 files changed, 6 insertions(+), 6 deletions(-) rename {app/assets => public}/fonts/Bold.woff (100%) rename {app/assets => public}/fonts/Bold.woff2 (100%) rename {app/assets => public}/fonts/Italic.woff (100%) rename {app/assets => public}/fonts/Italic.woff2 (100%) rename {app/assets => public}/fonts/Regular.woff (100%) rename {app/assets => public}/fonts/Regular.woff2 (100%) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 3b0d991..30b3a37 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -12,8 +12,8 @@ /* Phantom Sans Font Faces */ @font-face { font-family: 'Phantom Sans'; - src: url('/assets/Regular.woff2') format('woff2'), - url('/assets/Regular.woff') format('woff'); + src: url('/fonts/Regular.woff2') format('woff2'), + url('/fonts/Regular.woff') format('woff'); font-weight: normal; font-style: normal; font-display: swap; @@ -21,8 +21,8 @@ @font-face { font-family: 'Phantom Sans'; - src: url('/assets/Italic.woff2') format('woff2'), - url('/assets/Italic.woff') format('woff'); + src: url('/fonts/Italic.woff2') format('woff2'), + url('/fonts/Italic.woff') format('woff'); font-weight: normal; font-style: italic; font-display: swap; @@ -30,8 +30,8 @@ @font-face { font-family: 'Phantom Sans'; - src: url('/assets/Bold.woff2') format('woff2'), - url('/assets/Bold.woff') format('woff'); + src: url('/fonts/Bold.woff2') format('woff2'), + url('/fonts/Bold.woff') format('woff'); font-weight: bold; font-style: normal; font-display: swap; diff --git a/app/assets/fonts/Bold.woff b/public/fonts/Bold.woff similarity index 100% rename from app/assets/fonts/Bold.woff rename to public/fonts/Bold.woff diff --git a/app/assets/fonts/Bold.woff2 b/public/fonts/Bold.woff2 similarity index 100% rename from app/assets/fonts/Bold.woff2 rename to public/fonts/Bold.woff2 diff --git a/app/assets/fonts/Italic.woff b/public/fonts/Italic.woff similarity index 100% rename from app/assets/fonts/Italic.woff rename to public/fonts/Italic.woff diff --git a/app/assets/fonts/Italic.woff2 b/public/fonts/Italic.woff2 similarity index 100% rename from app/assets/fonts/Italic.woff2 rename to public/fonts/Italic.woff2 diff --git a/app/assets/fonts/Regular.woff b/public/fonts/Regular.woff similarity index 100% rename from app/assets/fonts/Regular.woff rename to public/fonts/Regular.woff diff --git a/app/assets/fonts/Regular.woff2 b/public/fonts/Regular.woff2 similarity index 100% rename from app/assets/fonts/Regular.woff2 rename to public/fonts/Regular.woff2 From 261abf02f1b05ff4dae880f942b228a2c4dfed0a Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 26 Jun 2025 14:14:09 -0400 Subject: [PATCH 2/4] widen --- app/views/leaderboards/index.html.erb | 2 +- app/views/my/mailing_addresses/show.html.erb | 2 +- app/views/my/project_repo_mappings/edit.html.erb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/leaderboards/index.html.erb b/app/views/leaderboards/index.html.erb index eac0f22..630d500 100644 --- a/app/views/leaderboards/index.html.erb +++ b/app/views/leaderboards/index.html.erb @@ -1,4 +1,4 @@ -
+

Leaderboard

diff --git a/app/views/my/mailing_addresses/show.html.erb b/app/views/my/mailing_addresses/show.html.erb index b8281d7..d6e01a9 100644 --- a/app/views/my/mailing_addresses/show.html.erb +++ b/app/views/my/mailing_addresses/show.html.erb @@ -3,7 +3,7 @@ <% end %>
-
+

My Mailing Address

Manage your shipping information for physical mail

diff --git a/app/views/my/project_repo_mappings/edit.html.erb b/app/views/my/project_repo_mappings/edit.html.erb index 6750440..fadafb6 100644 --- a/app/views/my/project_repo_mappings/edit.html.erb +++ b/app/views/my/project_repo_mappings/edit.html.erb @@ -1,5 +1,5 @@
-
+

Edit Repository Mapping

Connect your project to its GitHub repository

From ef3fef0d08baffd77f70728b824400c617dec4af Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 26 Jun 2025 14:18:55 -0400 Subject: [PATCH 3/4] readjust sizing based on fixed fonts --- app/views/static_pages/index.html.erb | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/views/static_pages/index.html.erb b/app/views/static_pages/index.html.erb index ae1537a..1f4c1c8 100644 --- a/app/views/static_pages/index.html.erb +++ b/app/views/static_pages/index.html.erb @@ -1,11 +1,14 @@
<% if current_user&.trust_level == "red" %>
- Hold up! Your account has been banned for suspicious activity.
+
+ + Hold up! Your account has been banned for suspicious activity. +
-

What does this mean? Your account was convicted for fraud or abuse of Hackatime, such as using methods to gain an unfair advantage on the leaderboards or attempting to manipulate your coding time in any way. This restricts your access to participate in public leaderboards, but Hackatime will still track and display your time. This may also affect your ability to participate in current and future Hack Club events.

-

What can I do? Account bans are permanent, non-negotiable, and cannot be removed. If a ban is determined to have been issued incorrectly, it will automatically be removed. We take fraud very seriously and have a zero-tolerance policy for abuse. If you believe this was a mistake, please email echo@hackclub.com. We do not respond in any other channel.

-

Can I know what caused this? No. We do not disclose the patterns that were detected. Releasing this information would only benefit fraudsters. The fraud team regularly investigates claims of false bans to increase the effectiveness of our detection systems to combat fraud.

+

What does this mean? Your account was convicted for fraud or abuse of Hackatime, such as using methods to gain an unfair advantage on the leaderboards or attempting to manipulate your coding time in any way. This restricts your access to participate in public leaderboards, but Hackatime will still track and display your time. This may also affect your ability to participate in current and future Hack Club events.

+

What can I do? Account bans are permanent, non-negotiable, and cannot be removed. If a ban is determined to have been issued incorrectly, it will automatically be removed. We take fraud very seriously and have a zero-tolerance policy for abuse. If you believe this was a mistake, please email echo@hackclub.com. We do not respond in any other channel.

+

Can I know what caused this? No. We do not disclose the patterns that were detected. Releasing this information would only benefit fraudsters. The fraud team regularly investigates claims of false bans to increase the effectiveness of our detection systems to combat fraud.

<% end %> From c52738587cd7d1f0ef779d69c49eef63c9d21abb Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 26 Jun 2025 15:44:21 -0400 Subject: [PATCH 4/4] optimize currently hacking info box --- app/controllers/static_pages_controller.rb | 34 ++- .../currently_hacking_controller.js | 206 ++++++++++++------ app/jobs/cache/currently_hacking_count_job.rb | 20 ++ app/views/layouts/application.html.erb | 13 +- config/routes.rb | 1 + 5 files changed, 195 insertions(+), 79 deletions(-) create mode 100644 app/jobs/cache/currently_hacking_count_job.rb diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb index 2657873..996a52d 100644 --- a/app/controllers/static_pages_controller.rb +++ b/app/controllers/static_pages_controller.rb @@ -175,18 +175,40 @@ class StaticPagesController < ApplicationController respond_to do |format| format.html { render partial: "currently_hacking", locals: locals } format.json do - json_response = { count: locals[:users].count } - - # Only include HTML if explicitly requested (when list is visible) - if params[:include_list] == "true" - json_response[:html] = render_to_string(partial: "currently_hacking", locals: locals, formats: [ :html ]) + json_response = locals[:users].map do |user| + { + id: user.id, + username: user.username, + slack_username: user.slack_username, + github_username: user.github_username, + display_name: user.display_name, + avatar_url: user.avatar_url, + slack_uid: user.slack_uid, + active_project: locals[:active_projects][user.id]&.then do |project| + { + name: project.project_name, + repo_url: project.repo_url + } + end + } end - render json: json_response + render json: { + count: locals[:users].count, + users: json_response + } end end end + def currently_hacking_count + result = Cache::CurrentlyHackingCountJob.perform_now + + respond_to do |format| + format.json { render json: { count: result[:count] } } + end + end + def streak render partial: "streak" end diff --git a/app/javascript/controllers/currently_hacking_controller.js b/app/javascript/controllers/currently_hacking_controller.js index e08150f..e771a03 100644 --- a/app/javascript/controllers/currently_hacking_controller.js +++ b/app/javascript/controllers/currently_hacking_controller.js @@ -1,23 +1,24 @@ import { Controller } from "@hotwired/stimulus" export default class extends Controller { - static targets = ["container", "count"] + static targets = ["container", "count", "content"] static values = { - interval: { type: Number, default: 60000 }, // 60 seconds to match cron - url: String + interval: { type: Number, default: 60000 }, // 60 seconds + countUrl: String, + fullUrl: String } connect() { - this.lastFullFetch = Date.now() // Initialize to now to prevent immediate refetch on click this.isExpanded = false - // this.startPolling() - this.poll() + this.isLoading = false + this.isVisible = false + this.startCountPolling() this.boundClickHandler = this.handleClick.bind(this) this.containerTarget.addEventListener('click', this.boundClickHandler) } disconnect() { - // this.stopPolling() + this.stopCountPolling() this.containerTarget.removeEventListener('click', this.boundClickHandler) } @@ -25,55 +26,58 @@ export default class extends Controller { const header = event.target.closest('.currently-hacking') if (header) { this.toggle() - // if (this.isExpanded) { - // const now = Date.now() - // const timeSinceLastFetch = now - this.lastFullFetch - // if (timeSinceLastFetch > 30000) { - // this.poll() - // } - // } } } - toggle() { + async toggle() { this.isExpanded = !this.isExpanded - const frame = document.getElementById("currently_hacking") - if (frame) { - frame.style.display = this.isExpanded ? 'block' : 'none' + + if (this.isExpanded) { + this.showLoading() + this.contentTarget.style.display = 'block' + await this.gimmeAll() + } else { + this.contentTarget.style.display = 'none' } } - isVisible() { - return this.isExpanded + showLoading() { + this.contentTarget.innerHTML = ` +
+
Loading...
+
+ ` } - // startPolling() { - // this.stopPolling() // Clear any existing interval - // this.poll() // Initial poll - // this.intervalId = setInterval(() => { - // this.poll() - // }, this.intervalValue) - // } + showBanner() { + if (!this.isVisible) { + this.isVisible = true + this.containerTarget.classList.remove('hidden') + setTimeout(() => { + this.containerTarget.classList.remove('-translate-y-full') + this.containerTarget.classList.add('translate-y-0') + }, 300) + } + } - // stopPolling() { - // if (this.intervalId) { - // clearInterval(this.intervalId) - // this.intervalId = null - // } - // } + startCountPolling() { + this.stopCountPolling() + this.pollCount() + this.countIntervalId = setInterval(() => { + this.pollCount() + }, this.intervalValue) + } - async poll() { + stopCountPolling() { + if (this.countIntervalId) { + clearInterval(this.countIntervalId) + this.countIntervalId = null + } + } + + async pollCount() { try { - const includeList = this.isVisible() - const url = new URL(this.urlValue, window.location.origin) - url.searchParams.set('include_list', includeList.toString()) - - // Track when we request the full list, not just when we get it back - if (includeList) { - this.lastFullFetch = Date.now() - } - - const response = await fetch(url, { + const response = await fetch(this.countUrlValue, { headers: { "Accept": "application/json" } @@ -82,12 +86,39 @@ export default class extends Controller { if (response.ok) { const data = await response.json() this.updateCount(data.count) - if (data.html) { - this.updateFrame(data.html) + this.showBanner() + } + } catch (e) { + console.error(e) + } + } + + async gimmeAll() { + if (this.isLoading) return + + this.isLoading = true + try { + const res = await fetch(this.fullUrlValue, { + headers: { + "Accept": "application/json" + } + }) + + if (res.ok) { + const data = await res.json() + if (data.users) { + this.r(data.users) } } } catch (error) { console.error("Failed to poll currently hacking:", error) + this.contentTarget.innerHTML = ` +
+
ruh ro, something broke :(
+
+ ` + } finally { + this.isLoading = false } } @@ -98,26 +129,69 @@ export default class extends Controller { } } - updateFrame(html) { - const frame = document.getElementById("currently_hacking") - if (frame && html) { - // Save scroll position before updating - const scrollContainer = frame.querySelector(".currently-hacking-list") - const scrollTop = scrollContainer ? scrollContainer.scrollTop : 0 - - // Update content - frame.innerHTML = html - frame.style.display = this.isExpanded ? 'block' : 'none' - - // Restore scroll position after a brief delay to allow DOM update - if (scrollTop > 0) { - requestAnimationFrame(() => { - const newScrollContainer = frame.querySelector(".currently-hacking-list") - if (newScrollContainer) { - newScrollContainer.scrollTop = scrollTop - } - }) - } + r(u) { + if (!u || u.length === 0) { + this.contentTarget.innerHTML = ` +
+
No one is currently hacking :(
+
+ ` + return } + + const us = u.map(user => this.r1(user)).join('') + + this.contentTarget.innerHTML = ` +
+
+ ${us} +
+
+ ` + } + + r1(u) { + const mention = this.r2(u) + const project = u.active_project ? this.r3(u.active_project) : '' + + return ` +
+
+ ${mention} +
+ ${project} +
+ ` + } + + r2(u) { + const dis = u.display_name || `User ${u.id}` + const url = u.avatar_url || '' + + const name = u.slack_uid ? + `@${dis}` : + `${dis}` + + return ` + + ` + } + + r3(p) { + const v = p.repo_url ? + p.repo_url.replace(/^https:\/\/github\.com\//, 'https://tkww0gcc0gkwwo4gc8kgs0sw.a.selfhosted.hackclub.com/') : '' + + return ` +
+ working on + ${p.repo_url ? `${p.name}` : p.name} + ${v ? `🌌` : ''} +
+ ` } } diff --git a/app/jobs/cache/currently_hacking_count_job.rb b/app/jobs/cache/currently_hacking_count_job.rb new file mode 100644 index 0000000..6930d62 --- /dev/null +++ b/app/jobs/cache/currently_hacking_count_job.rb @@ -0,0 +1,20 @@ +class Cache::CurrentlyHackingCountJob < Cache::ActivityJob + queue_as :latency_10s + + private + + def cache_expiration + 1.minute + end + + def calculate + count = Heartbeat.joins(:user) + .where(source_type: :direct_entry) + .coding_only + .where("time > ?", 5.minutes.ago.to_f) + .select("DISTINCT user_id") + .count + + { count: count } + end +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index cfb9cfe..21e7ccf 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -178,23 +178,22 @@ -
- - <%= pluralize(Cache::CurrentlyHackingJob.perform_now[:users].count, "person") %> currently hacking - +
- <%= turbo_frame_tag "currently_hacking", src: currently_hacking_static_pages_path, 'no-spinner' => true, style: "display: none;" do %> - <% end %> +
+
diff --git a/config/routes.rb b/config/routes.rb index 1142a2b..e5f8c71 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -50,6 +50,7 @@ Rails.application.routes.draw do get :project_durations get :activity_graph get :currently_hacking + get :currently_hacking_count get :filterable_dashboard_content get :filterable_dashboard get :mini_leaderboard