Merge pull request #371 from hackclub/beta-branch

make fonts work
This commit is contained in:
Echo
2025-06-26 15:46:15 -04:00
committed by GitHub
16 changed files with 211 additions and 92 deletions

View File

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

View File

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

View File

@@ -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 = `
<div class="p-4">
<div class="text-center text-muted text-md">Loading...</div>
</div>
`
}
// 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 = `
<div class="p-4 bg-elevated">
<div class="text-center text-muted text-sm">ruh ro, something broke :(</div>
</div>
`
} 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 = `
<div class="p-4 bg-elevated">
<div class="text-center text-muted text-sm italic">No one is currently hacking :(</div>
</div>
`
return
}
const us = u.map(user => this.r1(user)).join('')
this.contentTarget.innerHTML = `
<div class="currently-hacking-list max-h-[60vh] max-w-[400px] overflow-y-auto p-1 bg-darker">
<div class="space-y-1">
${us}
</div>
</div>
`
}
r1(u) {
const mention = this.r2(u)
const project = u.active_project ? this.r3(u.active_project) : ''
return `
<div class="flex flex-col space-y-1 p-1">
<div class="flex items-center">
${mention}
</div>
${project}
</div>
`
}
r2(u) {
const dis = u.display_name || `User ${u.id}`
const url = u.avatar_url || ''
const name = u.slack_uid ?
`<a href="https://slack.com/app_redirect?channel=${u.slack_uid}" target="_blank" class="text-blue-500 hover:underline">@${dis}</a>` :
`<span class="text-white">${dis}</span>`
return `
<div class="user-info flex items-center gap-2">
${url ? `<img src="${url}" alt="${dis}'s avatar" class="w-6 h-6 rounded-full aspect-square" loading="lazy">` : ''}
<span class="inline-flex items-center gap-1">
${name}
</span>
</div>
`
}
r3(p) {
const v = p.repo_url ?
p.repo_url.replace(/^https:\/\/github\.com\//, 'https://tkww0gcc0gkwwo4gc8kgs0sw.a.selfhosted.hackclub.com/') : ''
return `
<div class="text-sm italic text-muted ml-2">
working on
${p.repo_url ? `<a href="${p.repo_url}" target="_blank" class="text-accent hover:text-cyan-400 transition-colors">${p.name}</a>` : p.name}
${v ? `<a href="${v}" target="_blank" class="ml-1">🌌</a>` : ''}
</div>
`
}
}

View File

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

View File

@@ -178,23 +178,22 @@
</footer>
</main>
<div class="fixed top-0 right-5 max-w-sm max-h-[80vh] bg-elevated border border-darkless rounded-b-xl shadow-lg z-[1000] overflow-hidden"
<div class="fixed top-0 right-5 max-w-sm max-h-[80vh] bg-elevated border border-darkless rounded-b-xl shadow-lg z-[1000] overflow-hidden transform -translate-y-full transition-transform duration-300 ease-out hidden"
data-controller="currently-hacking"
data-currently-hacking-target="container"
data-currently-hacking-url-value="<%= currently_hacking_static_pages_path %>"
data-currently-hacking-count-url-value="<%= currently_hacking_count_static_pages_path %>"
data-currently-hacking-full-url-value="<%= currently_hacking_static_pages_path %>"
data-currently-hacking-interval-value="60000">
<div class="currently-hacking p-3 bg-elevated cursor-pointer select-none bg-dark flex items-center justify-between">
<div class="text-white text-sm font-medium">
<div class="flex items-center">
<div class="w-2 h-2 rounded-full bg-green-500 animate-pulse mr-2"></div>
<span data-currently-hacking-target="count">
<%= pluralize(Cache::CurrentlyHackingJob.perform_now[:users].count, "person") %> currently hacking
</span>
<span data-currently-hacking-target="count" class="text-lg"></span>
</div>
</div>
</div>
<%= turbo_frame_tag "currently_hacking", src: currently_hacking_static_pages_path, 'no-spinner' => true, style: "display: none;" do %>
<% end %>
<div data-currently-hacking-target="content" style="display: none;">
</div>
</div>
</body>
</html>

View File

@@ -1,4 +1,4 @@
<div class="max-w-4xl mx-auto p-6">
<div class="max-w-6xl mx-auto p-6">
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-4">Leaderboard</h1>

View File

@@ -3,7 +3,7 @@
<% end %>
<div class="min-h-screen bg-darker text-white">
<div class="max-w-4xl mx-auto px-6 py-8">
<div class="max-w-6xl mx-auto px-6 py-8">
<header class="mb-8">
<h1 class="text-3xl font-bold text-primary mb-2">My Mailing Address</h1>
<p class="text-secondary">Manage your shipping information for physical mail</p>

View File

@@ -1,5 +1,5 @@
<div class="min-h-screen bg-darker text-white">
<div class="max-w-4xl mx-auto px-6 py-8">
<div class="max-w-6xl mx-auto px-6 py-8">
<div class="mb-8">
<h1 class="text-3xl font-bold text-primary mb-2">Edit Repository Mapping</h1>
<p class="text-secondary">Connect your project to its GitHub repository</p>

View File

@@ -1,11 +1,14 @@
<div class="container">
<% if current_user&.trust_level == "red" %>
<div class="text-red-500 bg-red-500/10 border-2 border-red-500/20 p-4 text-center rounded-lg mb-4">
<span class="text-xl font-bold block mb-2">Hold up! Your account has been banned for suspicious activity.<br></span>
<div class="flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M8 14.5a6.5 6.5 0 1 0 0-13a6.5 6.5 0 0 0 0 13M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m1-5a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-.25-6.25a.75.75 0 0 0-1.5 0v3.5a.75.75 0 0 0 1.5 0z" clip-rule="evenodd"/></svg>
<span class="text-3xl font-bold block ml-2">Hold up! Your account has been banned for suspicious activity.</span>
</div>
<div>
<p class="text-red-500 text-left text-sm mb-2"><b>What does this mean?</b> 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.</p>
<p class="text-red-500 text-left text-sm mb-2"><b>What can I do?</b> 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 <a href="mailto:echo@hackclub.com" class="underline">echo@hackclub.com</a>. We do not respond in any other channel.</p>
<p class="text-red-500 text-left text-sm mb-0"><b>Can I know what caused this?</b> 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.</p>
<p class="text-red-500 text-left text-lg mb-2"><b>What does this mean?</b> 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.</p>
<p class="text-red-500 text-left text-lg mb-2"><b>What can I do?</b> 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 <a href="mailto:echo@hackclub.com" class="underline">echo@hackclub.com</a>. We do not respond in any other channel.</p>
<p class="text-red-500 text-left text-lg mb-0"><b>Can I know what caused this?</b> 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.</p>
</div>
</div>
<% end %>

View File

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