mirror of
https://github.com/SrIzan10/hc-harbor.git
synced 2026-05-01 10:45:21 +00:00
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
20
app/jobs/cache/currently_hacking_count_job.rb
vendored
Normal file
20
app/jobs/cache/currently_hacking_count_job.rb
vendored
Normal 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
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user