Improve code formatting and indentation (#863)

This commit is contained in:
Echo 2026-01-27 01:54:22 -05:00 committed by GitHub
parent c1e9eec98f
commit 9f0be25e76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
75 changed files with 1461 additions and 2150 deletions

View file

@ -7,9 +7,7 @@
<h1 class="text-3xl font-bold text-white mb-2">admin api keys</h1>
<p class="text-gray-400">fraud team is gonna foam at the mouth for this shit</p>
</div>
<%= link_to "spawn in a new key",
new_admin_admin_api_key_path,
class: "bg-primary hover:bg-red text-white px-4 py-2 rounded-lg font-medium transition-colors" %>
<%= link_to "spawn in a new key", new_admin_admin_api_key_path, class: "bg-primary hover:bg-red text-white px-4 py-2 rounded-lg font-medium transition-colors" %>
</div>
</div>
@ -36,7 +34,7 @@
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<% if api_key.user.avatar_url %>
<img class="h-6 w-6 rounded-full mr-2" src="<%= api_key.user.avatar_url %>" alt="">
<img class="h-6 w-6 rounded-full mr-2" src="<%= api_key.user.avatar_url %>" alt="key creator avatar">
<% end %>
<div>
<div class="text-sm text-white"><%= h(api_key.user.display_name) %></div>
@ -48,18 +46,11 @@
<%= api_key.created_at.strftime("%b %d, %Y at %I:%M %p") %>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<code class="text-xs text-white">
<%= api_key.token[0..12] %>...
</code>
<code class="text-xs text-white"> <%= api_key.token[0..12] %>... </code>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
<%= link_to "inspect",
admin_admin_api_key_path(api_key),
class: "text-blue-400 hover:text-blue-300" %>
<%= link_to "nuke",
admin_admin_api_key_path(api_key),
data: { "turbo-method": :delete },
class: "text-red-400 hover:text-red-300" %>
<%= link_to "inspect", admin_admin_api_key_path(api_key), class: "text-blue-400 hover:text-blue-300" %>
<%= link_to "nuke", admin_admin_api_key_path(api_key), data: { "turbo-method": :delete }, class: "text-red-400 hover:text-red-300" %>
</td>
</tr>
<% end %>

View file

@ -7,9 +7,7 @@
<h1 class="text-3xl font-bold text-white mb-2">spawn in a new key</h1>
<p class="text-gray-400">its geting real</p>
</div>
<%= link_to "← get me outta here",
admin_admin_api_keys_path,
class: "text-gray-400 hover:text-white" %>
<%= link_to "← get me outta here", admin_admin_api_keys_path, class: "text-gray-400 hover:text-white" %>
</div>
</div>
@ -28,9 +26,7 @@
<div>
<%= f.label :name, class: "block text-md font-medium text-white mb-2" %>
<%= f.text_field :name,
placeholder: "put down something good please",
class: "w-full px-3 py-2 bg-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary" %>
<%= f.text_field :name, placeholder: "put down something good please", class: "w-full px-3 py-2 bg-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary" %>
</div>
<div class="bg-yellow-900/30 border border-yellow-500/50 rounded-lg p-4">
@ -38,11 +34,8 @@
</div>
<div class="flex justify-end space-x-4">
<%= link_to "fuck no imma head out",
admin_admin_api_keys_path,
class: "px-4 py-2 text-gray-400 hover:text-white" %>
<%= f.submit "okay lets do this thing",
class: "px-6 py-2 bg-primary hover:bg-red text-white rounded-lg font-medium transition-colors" %>
<%= link_to "fuck no imma head out", admin_admin_api_keys_path, class: "px-4 py-2 text-gray-400 hover:text-white" %>
<%= f.submit "okay lets do this thing", class: "px-6 py-2 bg-primary hover:bg-red text-white rounded-lg font-medium transition-colors" %>
</div>
<% end %>
</div>

View file

@ -1,4 +1,4 @@
<% content_for :title, "Admin API Key Details" %>
<% content_for :title, 'Admin API Key Details' %>
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-8">
@ -7,9 +7,7 @@
<h1 class="text-3xl font-bold text-white mb-2">lookin at <%= @admin_api_key.name %></h1>
<p class="text-gray-400">get the deets</p>
</div>
<%= link_to "← Back to API Keys",
admin_admin_api_keys_path,
class: "text-gray-400 hover:text-white" %>
<%= link_to '← Back to API Keys', admin_admin_api_keys_path, class: 'text-gray-400 hover:text-white' %>
</div>
</div>
@ -19,23 +17,15 @@
<div class="space-y-4">
<div>
<label class="block text-md font-medium text-gray-400 mb-2">what was it again?</label>
<code class="block bg-darkless px-3 py-2 rounded text-white text-sm">
<%= @admin_api_key.token[0..20] %>...
</code>
<code class="block bg-darkless px-3 py-2 rounded text-white text-sm"> <%= @admin_api_key.token[0..20] %>... </code>
<% unless flash[:api_key_token] %>
<p class="text-md text-gray-400 mt-1">
you cant see the full thing again, we showed it when you created it ya doofus
</p>
<p class="text-md text-gray-400 mt-1">you cant see the full thing again, we showed it when you created it ya doofus</p>
<% end %>
</div>
<div>
<label class="block text-md font-bold text-white mb-2">how to use it?</label>
<p class="text-md text-gray-400 mb-2">
most likely you are not crazy enough to build your own api client but you can use rowan's fraud check tool to use the admin api.
</p>
<p class="text-md text-gray-400">
if you are building your own client, just use the token as a bearer token in the auth header of your requests, check the actual source code for more details. or you could just be normal and use the fraud check tool i already made for you.
</p>
<p class="text-md text-gray-400 mb-2">most likely you are not crazy enough to build your own api client but you can use rowan's fraud check tool to use the admin api.</p>
<p class="text-md text-gray-400">if you are building your own client, just use the token as a bearer token in the auth header of your requests, check the actual source code for more details. or you could just be normal and use the fraud check tool i already made for you.</p>
</div>
</div>
</div>
@ -45,13 +35,9 @@
<% if @show_token %>
<div class="bg-green-900/30 border border-green-500/50 rounded-lg p-4 mb-6">
<h3 class="text-green-300 font-medium mb-2">heres ya key, copy it now!</h3>
<p class="text-green-200 text-sm mb-3">
copy it now, its not gonna be shown again silly
</p>
<p class="text-green-200 text-sm mb-3">copy it now, its not gonna be shown again silly</p>
<div class="bg-gray-800 rounded p-3">
<code class="block text-white text-sm break-all select-all">
<%= @admin_api_key.token %>
</code>
<code class="block text-white text-sm break-all select-all"> <%= @admin_api_key.token %> </code>
</div>
</div>
<% end %>
@ -76,7 +62,7 @@
<div>
<label class="block text-sm font-medium text-gray-400 mb-1">spawned at</label>
<div class="text-white"><%= @admin_api_key.created_at.strftime("%B %d, %Y at %I:%M %p") %></div>
<div class="text-white"><%= @admin_api_key.created_at.strftime('%B %d, %Y at %I:%M %p') %></div>
</div>
<div>
@ -85,10 +71,7 @@
</div>
</div>
<div class="mt-6"></div>
<%= link_to "nuke it",
admin_admin_api_key_path(@admin_api_key),
data: { "turbo-method": :delete },
class: "bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg font-medium transition-colors" %>
<%= link_to 'nuke it', admin_admin_api_key_path(@admin_api_key), data: { 'turbo-method': :delete }, class: 'bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg font-medium transition-colors' %>
</div>
</div>
</div>

View file

@ -7,29 +7,28 @@
<div>
<span class="text-white font-medium"><%= user.display_name %></span>
<% if user.admin_level != "default" %>
<span class="ml-2 px-2 py-0.5 text-xs rounded-full
<span
class="ml-2 px-2 py-0.5 text-xs rounded-full
<%= case user.admin_level
when 'superadmin' then 'bg-red-600/20 text-red-400'
when 'admin' then 'bg-yellow-600/20 text-yellow-400'
when 'viewer' then 'bg-blue-600/20 text-blue-400'
else 'bg-gray-600/20 text-gray-400'
when 'superadmin'
'bg-red-600/20 text-red-400'
when 'admin'
'bg-yellow-600/20 text-yellow-400'
when 'viewer'
'bg-blue-600/20 text-blue-400'
else
'bg-gray-600/20 text-gray-400'
end %>">
<%= user.admin_level %>
</span>
<% end %>
<div class="text-gray-500 text-sm"><%= user.slack_uid || "No Slack ID" %></div>
<div class="text-gray-500 text-sm"><%= user.slack_uid || 'No Slack ID' %></div>
</div>
</div>
<div class="flex gap-2">
<%= button_to "→ Superadmin", admin_admin_user_path(user, admin_level: "superadmin"),
method: :patch,
class: "px-3 py-1 bg-red-600 hover:bg-red-500 text-white text-sm font-medium rounded transition-colors cursor-pointer" %>
<%= button_to "→ Admin", admin_admin_user_path(user, admin_level: "admin"),
method: :patch,
class: "px-3 py-1 bg-yellow-600 hover:bg-yellow-500 text-white text-sm font-medium rounded transition-colors cursor-pointer" %>
<%= button_to "→ Viewer", admin_admin_user_path(user, admin_level: "viewer"),
method: :patch,
class: "px-3 py-1 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium rounded transition-colors cursor-pointer" %>
<%= button_to '→ Superadmin', admin_admin_user_path(user, admin_level: 'superadmin'), method: :patch, class: 'px-3 py-1 bg-red-600 hover:bg-red-500 text-white text-sm font-medium rounded transition-colors cursor-pointer' %>
<%= button_to '→ Admin', admin_admin_user_path(user, admin_level: 'admin'), method: :patch, class: 'px-3 py-1 bg-yellow-600 hover:bg-yellow-500 text-white text-sm font-medium rounded transition-colors cursor-pointer' %>
<%= button_to '→ Viewer', admin_admin_user_path(user, admin_level: 'viewer'), method: :patch, class: 'px-3 py-1 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium rounded transition-colors cursor-pointer' %>
</div>
</div>
<% end %>

View file

@ -7,12 +7,7 @@
<div class="border border-primary rounded-xl p-6 bg-dark">
<h2 class="text-2xl font-semibold text-green-400 mb-4">Promote</h2>
<div class="mb-4">
<input type="text"
id="user-search"
placeholder="Search by name or Slack ID..."
class="w-full px-4 py-2 bg-darker border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-primary"
data-controller="admin-user-search"
data-action="input->admin-user-search#search">
<input type="text" id="user-search" placeholder="Search by name or Slack ID..." class="w-full px-4 py-2 bg-darker border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-primary" data-controller="admin-user-search" data-action="input->admin-user-search#search">
</div>
<div id="search-results" class="space-y-2"></div>
</div>
@ -41,22 +36,13 @@
<% end %>
</div>
</td>
<td class="py-3 px-4 text-gray-300"><%= user.slack_uid || "N/A" %></td>
<td class="py-3 px-4 text-gray-300"><%= user.slack_uid || 'N/A' %></td>
<td class="py-3 px-4">
<% if user.id != @current_user_id %>
<div class="flex gap-2">
<%= button_to "→ Admin", admin_admin_user_path(user, admin_level: "admin"),
method: :patch,
class: "px-3 py-1 bg-yellow-600 hover:bg-yellow-500 text-white text-sm font-medium rounded transition-colors cursor-pointer",
data: { confirm: "Demote #{user.display_name} to Admin?" } %>
<%= button_to "→ Viewer", admin_admin_user_path(user, admin_level: "viewer"),
method: :patch,
class: "px-3 py-1 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium rounded transition-colors cursor-pointer",
data: { confirm: "Demote #{user.display_name} to Viewer?" } %>
<%= button_to "→ Default", admin_admin_user_path(user, admin_level: "default"),
method: :patch,
class: "px-3 py-1 bg-gray-600 hover:bg-gray-500 text-white text-sm font-medium rounded transition-colors cursor-pointer",
data: { confirm: "Remove #{user.display_name}'s admin privileges?" } %>
<%= button_to '→ Admin', admin_admin_user_path(user, admin_level: 'admin'), method: :patch, class: 'px-3 py-1 bg-yellow-600 hover:bg-yellow-500 text-white text-sm font-medium rounded transition-colors cursor-pointer', data: { confirm: "Demote #{user.display_name} to Admin?" } %>
<%= button_to '→ Viewer', admin_admin_user_path(user, admin_level: 'viewer'), method: :patch, class: 'px-3 py-1 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium rounded transition-colors cursor-pointer', data: { confirm: "Demote #{user.display_name} to Viewer?" } %>
<%= button_to '→ Default', admin_admin_user_path(user, admin_level: 'default'), method: :patch, class: 'px-3 py-1 bg-gray-600 hover:bg-gray-500 text-white text-sm font-medium rounded transition-colors cursor-pointer', data: { confirm: "Remove #{user.display_name}'s admin privileges?" } %>
</div>
<% else %>
<span class="text-gray-500 text-sm">Cannot modify yourself</span>
@ -93,21 +79,12 @@
<span class="text-white"><%= user.display_name %></span>
</div>
</td>
<td class="py-3 px-4 text-gray-300"><%= user.slack_uid || "N/A" %></td>
<td class="py-3 px-4 text-gray-300"><%= user.slack_uid || 'N/A' %></td>
<td class="py-3 px-4">
<div class="flex gap-2">
<%= button_to "→ Superadmin", admin_admin_user_path(user, admin_level: "superadmin"),
method: :patch,
class: "px-3 py-1 bg-red-600 hover:bg-red-500 text-white text-sm font-medium rounded transition-colors cursor-pointer",
data: { confirm: "Promote #{user.display_name} to Superadmin?" } %>
<%= button_to "→ Viewer", admin_admin_user_path(user, admin_level: "viewer"),
method: :patch,
class: "px-3 py-1 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium rounded transition-colors cursor-pointer",
data: { confirm: "Demote #{user.display_name} to Viewer?" } %>
<%= button_to "→ Default", admin_admin_user_path(user, admin_level: "default"),
method: :patch,
class: "px-3 py-1 bg-gray-600 hover:bg-gray-500 text-white text-sm font-medium rounded transition-colors cursor-pointer",
data: { confirm: "Remove #{user.display_name}'s admin privileges?" } %>
<%= button_to '→ Superadmin', admin_admin_user_path(user, admin_level: 'superadmin'), method: :patch, class: 'px-3 py-1 bg-red-600 hover:bg-red-500 text-white text-sm font-medium rounded transition-colors cursor-pointer', data: { confirm: "Promote #{user.display_name} to Superadmin?" } %>
<%= button_to '→ Viewer', admin_admin_user_path(user, admin_level: 'viewer'), method: :patch, class: 'px-3 py-1 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium rounded transition-colors cursor-pointer', data: { confirm: "Demote #{user.display_name} to Viewer?" } %>
<%= button_to '→ Default', admin_admin_user_path(user, admin_level: 'default'), method: :patch, class: 'px-3 py-1 bg-gray-600 hover:bg-gray-500 text-white text-sm font-medium rounded transition-colors cursor-pointer', data: { confirm: "Remove #{user.display_name}'s admin privileges?" } %>
</div>
</td>
</tr>
@ -141,21 +118,12 @@
<span class="text-white"><%= user.display_name %></span>
</div>
</td>
<td class="py-3 px-4 text-gray-300"><%= user.slack_uid || "N/A" %></td>
<td class="py-3 px-4 text-gray-300"><%= user.slack_uid || 'N/A' %></td>
<td class="py-3 px-4">
<div class="flex gap-2">
<%= button_to "→ Superadmin", admin_admin_user_path(user, admin_level: "superadmin"),
method: :patch,
class: "px-3 py-1 bg-red-600 hover:bg-red-500 text-white text-sm font-medium rounded transition-colors cursor-pointer",
data: { confirm: "Promote #{user.display_name} to Superadmin?" } %>
<%= button_to "→ Admin", admin_admin_user_path(user, admin_level: "admin"),
method: :patch,
class: "px-3 py-1 bg-yellow-600 hover:bg-yellow-500 text-white text-sm font-medium rounded transition-colors cursor-pointer",
data: { confirm: "Promote #{user.display_name} to Admin?" } %>
<%= button_to "→ Default", admin_admin_user_path(user, admin_level: "default"),
method: :patch,
class: "px-3 py-1 bg-gray-600 hover:bg-gray-500 text-white text-sm font-medium rounded transition-colors cursor-pointer",
data: { confirm: "Remove #{user.display_name}'s viewer privileges?" } %>
<%= button_to '→ Superadmin', admin_admin_user_path(user, admin_level: 'superadmin'), method: :patch, class: 'px-3 py-1 bg-red-600 hover:bg-red-500 text-white text-sm font-medium rounded transition-colors cursor-pointer', data: { confirm: "Promote #{user.display_name} to Superadmin?" } %>
<%= button_to '→ Admin', admin_admin_user_path(user, admin_level: 'admin'), method: :patch, class: 'px-3 py-1 bg-yellow-600 hover:bg-yellow-500 text-white text-sm font-medium rounded transition-colors cursor-pointer', data: { confirm: "Promote #{user.display_name} to Admin?" } %>
<%= button_to '→ Default', admin_admin_user_path(user, admin_level: 'default'), method: :patch, class: 'px-3 py-1 bg-gray-600 hover:bg-gray-500 text-white text-sm font-medium rounded transition-colors cursor-pointer', data: { confirm: "Remove #{user.display_name}'s viewer privileges?" } %>
</div>
</td>
</tr>
@ -170,29 +138,29 @@
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const input = document.getElementById('user-search');
const res = document.getElementById('search-results');
document.addEventListener("DOMContentLoaded", function () {
const input = document.getElementById("user-search");
const res = document.getElementById("search-results");
let d;
if (input) {
input.addEventListener('input', function() {
input.addEventListener("input", function () {
clearTimeout(d);
const query = this.value.trim();
if (query.length < 2) {
res.innerHTML = '';
res.innerHTML = "";
return;
}
d = setTimeout(function() {
fetch('/admin/admin_users/search?q=' + encodeURIComponent(query), {
headers: { 'Accept': 'text/html' }
d = setTimeout(function () {
fetch("/admin/admin_users/search?q=" + encodeURIComponent(query), {
headers: { Accept: "text/html" },
})
.then(r => r.text())
.then(html => {
res.innerHTML = html;
});
.then((r) => r.text())
.then((html) => {
res.innerHTML = html;
});
}, 100);
});
}

View file

@ -26,28 +26,28 @@
<span class="text-white"><%= request.user.display_name %></span>
</div>
</td>
<td class="py-3 px-4 text-gray-300"><%= request.user.email_addresses.first&.email || "N/A" %></td>
<td class="py-3 px-4 text-gray-300"><%= request.user.email_addresses.first&.email || 'N/A' %></td>
<td class="py-3 px-4 text-gray-300"><%= time_ago_in_words(request.requested_at) %> ago</td>
<td class="py-3 px-4">
<span class="px-2 py-1 rounded text-xs font-medium
<span
class="px-2 py-1 rounded text-xs font-medium
<%= case request.user.trust_level
when 'green' then 'bg-green-600/20 text-green-400'
when 'yellow' then 'bg-yellow-600/20 text-yellow-400'
when 'red' then 'bg-red-600/20 text-red-400'
else 'bg-blue-600/20 text-blue-400'
when 'green'
'bg-green-600/20 text-green-400'
when 'yellow'
'bg-yellow-600/20 text-yellow-400'
when 'red'
'bg-red-600/20 text-red-400'
else
'bg-blue-600/20 text-blue-400'
end %>">
<%= request.user.trust_level %>
</span>
</td>
<td class="py-3 px-4">
<div class="flex gap-2">
<%= button_to "yuh", approve_admin_deletion_request_path(request),
method: :post,
class: "px-3 py-1 bg-green-600 hover:bg-green-500 text-white text-sm font-medium rounded transition-colors cursor-pointer" %>
<%= button_to "nah", reject_admin_deletion_request_path(request),
method: :post,
class: "px-3 py-1 bg-red-600 hover:bg-red-500 text-white text-sm font-medium rounded transition-colors cursor-pointer",
data: { confirm: "yo " } %>
<%= button_to 'yuh', approve_admin_deletion_request_path(request), method: :post, class: 'px-3 py-1 bg-green-600 hover:bg-green-500 text-white text-sm font-medium rounded transition-colors cursor-pointer' %>
<%= button_to 'nah', reject_admin_deletion_request_path(request), method: :post, class: 'px-3 py-1 bg-red-600 hover:bg-red-500 text-white text-sm font-medium rounded transition-colors cursor-pointer', data: { confirm: 'yo ' } %>
</div>
</td>
</tr>
@ -83,13 +83,11 @@
<span class="text-white"><%= request.user.display_name %></span>
</div>
</td>
<td class="py-3 px-4 text-gray-300"><%= request.admin_approved_by&.display_name || "N/A" %></td>
<td class="py-3 px-4 text-gray-300"><%= request.admin_approved_at&.strftime("%b %d, %Y") %></td>
<td class="py-3 px-4 text-red-400"><%= request.scheduled_deletion_at&.strftime("%b %d, %Y") %></td>
<td class="py-3 px-4 text-gray-300"><%= request.admin_approved_by&.display_name || 'N/A' %></td>
<td class="py-3 px-4 text-gray-300"><%= request.admin_approved_at&.strftime('%b %d, %Y') %></td>
<td class="py-3 px-4 text-red-400"><%= request.scheduled_deletion_at&.strftime('%b %d, %Y') %></td>
<td class="py-3 px-4">
<span class="px-2 py-1 rounded text-xs font-medium bg-red-600/20 text-red-400">
<%= request.days_until_deletion %> days
</span>
<span class="px-2 py-1 rounded text-xs font-medium bg-red-600/20 text-red-400"> <%= request.days_until_deletion %> days </span>
</td>
</tr>
<% end %>
@ -117,8 +115,8 @@
<% @done.each do |request| %>
<tr class="border-b border-gray-800 hover:bg-gray-800/50">
<td class="py-3 px-4 text-gray-300">#<%= request.user_id %></td>
<td class="py-3 px-4 text-gray-300"><%= request.admin_approved_by&.display_name || "N/A" %></td>
<td class="py-3 px-4 text-gray-300"><%= request.completed_at&.strftime("%b %d, %Y at %I:%M %p") %></td>
<td class="py-3 px-4 text-gray-300"><%= request.admin_approved_by&.display_name || 'N/A' %></td>
<td class="py-3 px-4 text-gray-300"><%= request.completed_at&.strftime('%b %d, %Y at %I:%M %p') %></td>
</tr>
<% end %>
</tbody>

View file

@ -59,55 +59,42 @@
<div class="space-y-5">
<div>
<%= f.label :name, class: "block text-sm font-medium text-white mb-2" %>
<%= f.text_field :name,
class: "w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary placeholder-secondary",
placeholder: "My Awesome App",
required: true %>
<%= f.label :name, class: 'block text-sm font-medium text-white mb-2' %>
<%= f.text_field :name, class: 'w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary placeholder-secondary', placeholder: 'My Awesome App', required: true %>
<% if @application.errors[:name].present? %>
<p class="mt-1 text-xs text-red"><%= @application.errors[:name].to_sentence %></p>
<% end %>
</div>
<div>
<%= f.label :redirect_uri, "Redirect URIs", class: "block text-sm font-medium text-white mb-2" %>
<%= f.text_area :redirect_uri,
class: "w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary placeholder-secondary font-mono text-sm",
placeholder: "https://example.com/auth/callback",
rows: 3 %>
<%= f.label :redirect_uri, 'Redirect URIs', class: 'block text-sm font-medium text-white mb-2' %>
<%= f.text_area :redirect_uri, class: 'w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary placeholder-secondary font-mono text-sm', placeholder: 'https://example.com/auth/callback', rows: 3 %>
<% if @application.errors[:redirect_uri].present? %>
<p class="mt-1 text-xs text-red"><%= @application.errors[:redirect_uri].to_sentence %></p>
<% end %>
</div>
<div>
<%= f.label :scopes, class: "block text-sm font-medium text-white mb-2" %>
<%= f.text_field :scopes,
class: "w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary placeholder-secondary font-mono text-sm",
placeholder: "profile read" %>
<%= f.label :scopes, class: 'block text-sm font-medium text-white mb-2' %>
<%= f.text_field :scopes, class: 'w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary placeholder-secondary font-mono text-sm', placeholder: 'profile read' %>
<% if @application.errors[:scopes].present? %>
<p class="mt-1 text-xs text-red"><%= @application.errors[:scopes].to_sentence %></p>
<% end %>
</div>
<div class="flex items-start gap-3 p-4 bg-darkless border border-darkless rounded">
<%= f.check_box :confidential,
class: "mt-0.5 w-4 h-4 text-primary border-darkless rounded focus:ring-primary bg-darker" %>
<%= f.check_box :confidential, class: 'mt-0.5 w-4 h-4 text-primary border-darkless rounded focus:ring-primary bg-darker' %>
<div>
<%= f.label :confidential, "Confidential Application", class: "text-sm font-medium text-white" %>
<p class="mt-1 text-xs text-secondary">
Confidential clients can keep secrets. Native apps and SPAs are not confidential.
</p>
<%= f.label :confidential, 'Confidential Application', class: 'text-sm font-medium text-white' %>
<p class="mt-1 text-xs text-secondary">Confidential clients can keep secrets. Native apps and SPAs are not confidential.</p>
</div>
</div>
</div>
</div>
<div class="flex items-center gap-3">
<%= f.submit "Save Changes",
class: "px-6 py-2 bg-primary text-white font-medium rounded transition-colors duration-200 hover:opacity-90 cursor-pointer" %>
<%= link_to "Cancel", admin_oauth_application_path(@application),
class: "px-6 py-2 border border-darkless text-white font-medium rounded transition-colors duration-200 hover:bg-darkless" %>
<%= f.submit 'Save Changes', class: 'px-6 py-2 bg-primary text-white font-medium rounded transition-colors duration-200 hover:opacity-90 cursor-pointer' %>
<%= link_to 'Cancel', admin_oauth_application_path(@application), class: 'px-6 py-2 border border-darkless text-white font-medium rounded transition-colors duration-200 hover:bg-darkless' %>
</div>
<% end %>
</div>

View file

@ -21,8 +21,7 @@
</svg>
</div>
<div class="flex items-center gap-2">
<%= link_to application.name, admin_oauth_application_path(application),
class: "text-xl font-semibold text-white hover:text-primary transition-colors" %>
<%= link_to application.name, admin_oauth_application_path(application), class: "text-xl font-semibold text-white hover:text-primary transition-colors" %>
<% if application.verified? %>
<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-green/20 text-green border border-green/30 rounded text-xs">
<svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">

View file

@ -23,11 +23,7 @@
gutter_px = 4
# Current admin user and selected users for Stimulus
current_admin_user = {
id: current_user.id,
display_name: current_user.display_name,
avatar_url: current_user.avatar_url
}
current_admin_user = { id: current_user.id, display_name: current_user.display_name, avatar_url: current_user.avatar_url }
current_admin_user_json = current_admin_user.to_json
initial_selected_users_json = @initial_selected_user_objects.to_json
@ -35,18 +31,18 @@
%>
<style>
@keyframes spin {to {transform: rotate(360deg);}}
.spinny {animation: spin 0.2s linear infinite;}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.spinny {
animation: spin 0.2s linear infinite;
}
</style>
<div class=" text-white flex flex-col font-sans min-h-screen">
<div
data-controller="admin-timeline-user-selector"
data-admin-timeline-user-selector-current-user-json-value='<%= current_admin_user_json %>'
data-admin-timeline-user-selector-initial-selected-users-json-value='<%= initial_selected_users_json %>'
data-admin-timeline-user-selector-search-url-value="<%= admin_timeline_search_users_path %>"
data-admin-timeline-user-selector-leaderboard-users-url-value="<%= admin_timeline_leaderboard_users_path %>"
class="mb-4 p-3 bg-dark rounded-md flex-shrink-0">
<div data-controller="admin-timeline-user-selector" data-admin-timeline-user-selector-current-user-json-value="<%= current_admin_user_json %>" data-admin-timeline-user-selector-initial-selected-users-json-value="<%= initial_selected_users_json %>" data-admin-timeline-user-selector-search-url-value="<%= admin_timeline_search_users_path %>" data-admin-timeline-user-selector-leaderboard-users-url-value="<%= admin_timeline_leaderboard_users_path %>" class="mb-4 p-3 bg-dark rounded-md shrink-0">
<form id="timeline-filter-form" action="<%= admin_timeline_path %>" method="get" data-turbo-frame="_top">
<input type="hidden" name="user_ids" data-admin-timeline-user-selector-target="userIdsInput">
<input type="hidden" name="date" value="<%= current_date_for_form %>" data-admin-timeline-user-selector-target="dateInput">
@ -63,35 +59,15 @@
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
<input type="text"
id="user-search-input"
placeholder="Add user by name/email/id..."
data-admin-timeline-user-selector-target="searchInput"
data-action="input->admin-timeline-user-selector#debouncedSearch keydown->admin-timeline-user-selector#handleKeydown focus->admin-timeline-user-selector#search blur->admin-timeline-user-selector#hideResultsDelayed"
autocomplete="off"
class="w-full pl-10 pr-3 py-2 bg-darker rounded-md text-white placeholder-gray-300 focus:outline-none focus:border-transparent text-sm">
<input type="text" id="user-search-input" placeholder="Add user by name/email/id..." data-admin-timeline-user-selector-target="searchInput" data-action="input->admin-timeline-user-selector#debouncedSearch keydown->admin-timeline-user-selector#handleKeydown focus->admin-timeline-user-selector#search blur->admin-timeline-user-selector#hideResultsDelayed" autocomplete="off" class="w-full pl-10 pr-3 py-2 bg-darker rounded-md text-white placeholder-gray-300 focus:outline-none focus:border-transparent text-sm">
</div>
<div class="absolute top-full left-0 w-full bg-dark border border-gray-700 rounded-lg mt-1 z-50 max-h-48 overflow-y-auto hidden shadow-lg"
data-admin-timeline-user-selector-target="searchResults">
<div class="absolute top-full left-0 w-full bg-dark border border-gray-700 rounded-lg mt-1 z-50 max-h-48 overflow-y-auto hidden shadow-lg" data-admin-timeline-user-selector-target="searchResults">
<%# Search results will appear here %>
</div>
</div>
<button type="button"
class="px-3 py-2 bg-darker rounded-md text-sm text-white transition-colors"
data-action="admin-timeline-user-selector#applyPreset"
data-period="today">
Top 15 Today
</button>
<button type="button"
class="px-3 py-2 bg-darker rounded-md text-sm text-white transition-colors"
data-action="admin-timeline-user-selector#applyPreset"
data-period="last_7_days">
Top 15 Week
</button>
<button type="submit"
class="px-4 py-2 bg-green-600 rounded-md text-sm text-white transition-colors font-medium">
View
</button>
<button type="button" class="px-3 py-2 bg-darker rounded-md text-sm text-white transition-colors" data-action="admin-timeline-user-selector#applyPreset" data-period="today">Top 15 Today</button>
<button type="button" class="px-3 py-2 bg-darker rounded-md text-sm text-white transition-colors" data-action="admin-timeline-user-selector#applyPreset" data-period="last_7_days">Top 15 Week</button>
<button type="submit" class="px-4 py-2 bg-green-600 rounded-md text-sm text-white transition-colors font-medium">View</button>
</div>
<div class="mt-2 min-h-7" data-admin-timeline-user-selector-target="selectedUsersContainer">
@ -100,20 +76,14 @@
</form>
</div>
<div class="flex justify-between items-center mb-4 flex-shrink-0">
<div class="flex justify-between items-center mb-4 shrink-0">
<div class="text-lg font-semibold">
<%= @date.in_time_zone(primary_user_tz).strftime("%A, %B %-d, %Y") %>
<%= @date.in_time_zone(primary_user_tz).strftime('%A, %B %-d, %Y') %>
</div>
<div class="flex gap-2">
<%= link_to "← Prev", admin_timeline_path(date: @prev_date.to_s),
class: "px-3 py-1 bg-darker rounded text-sm transition-colors",
data: { "date-nav-link": "true" } %>
<%= link_to "Today", admin_timeline_path(date: Time.current.to_date.to_s),
class: "px-3 py-1 bg-darker rounded text-sm transition-colors",
data: { "date-nav-link": "true" } %>
<%= link_to "Next →", admin_timeline_path(date: @next_date.to_s),
class: "px-3 py-1 bg-darker rounded text-sm transition-colors",
data: { "date-nav-link": "true" } %>
<%= link_to '← Prev', admin_timeline_path(date: @prev_date.to_s), class: 'px-3 py-1 bg-darker rounded text-sm transition-colors', data: { 'date-nav-link': 'true' } %>
<%= link_to 'Today', admin_timeline_path(date: Time.current.to_date.to_s), class: 'px-3 py-1 bg-darker rounded text-sm transition-colors', data: { 'date-nav-link': 'true' } %>
<%= link_to 'Next →', admin_timeline_path(date: @next_date.to_s), class: 'px-3 py-1 bg-darker rounded text-sm transition-colors', data: { 'date-nav-link': 'true' } %>
</div>
</div>
@ -129,10 +99,9 @@
<% (0..23).each do |hour| %>
<%
hour_top = 120 + (hour * pixels_per_hour)
formatted_hour = Time.utc(2000,1,1, hour).strftime("%-l:00 %p")
formatted_hour = Time.utc(2000, 1, 1, hour).strftime('%-l:00 %p')
%>
<div class="absolute left-0 w-full border-t border-gray-600"
style="top: <%= hour_top %>px; height: <%= pixels_per_hour %>px;">
<div class="absolute left-0 w-full border-t border-gray-600" style="top: <%= hour_top %>px; height: <%= pixels_per_hour %>px;">
<div class="absolute left-2 top-2 text-xs text-gray-300 font-mono px-1">
<%= formatted_hour %>
</div>
@ -144,59 +113,59 @@
user = data[:user]
total_coded_time_seconds = data[:total_coded_time]
column_left = hour_label_width + (index * (min_column_width_px + gutter_px))
trust_level_emoji = case user.respond_to?(:trust_level) ? user.trust_level : nil
when "red" then "🔴"
when "green" then "🟢"
when "yellow" then "🟡"
else "🔵"
end
trust_level_emoji =
case user.respond_to?(:trust_level) ? user.trust_level : nil
when 'red'
'🔴'
when 'green'
'🟢'
when 'yellow'
'🟡'
else
'🔵'
end
trust_level_bg = case user.respond_to?(:trust_level) ? user.trust_level : nil
when "red" then "bg-red-500/20"
when "green" then "bg-green-500/20"
when "yellow" then "bg-yellow-500/20"
when "blue" then "bg-blue-500/20"
else "bg-gray-500/20"
end
trust_level_bg =
case user.respond_to?(:trust_level) ? user.trust_level : nil
when 'red'
'bg-red-500/20'
when 'green'
'bg-green-500/20'
when 'yellow'
'bg-yellow-500/20'
when 'blue'
'bg-blue-500/20'
else
'bg-gray-500/20'
end
%>
<div class="absolute border-r border-gray-700"
style="left: <%= column_left %>px; width: <%= min_column_width_px %>px; top: 120px; bottom: 0; padding-bottom: 0;">
</div>
<div class="absolute top-0 p-3 rounded-lg shadow-lg <%= trust_level_bg %>"
data-user-id="<%= user.id %>"
style="left: <%= column_left + 2 %>px; width: <%= min_column_width_px - 4 %>px;"
title="User ID: <%= user.id %> - <%= user.respond_to?(:display_name) && user.display_name.present? ? h(user.display_name) : h(user.email_addresses.first&.email) %> | Total Coded: <%= total_coded_time_seconds && total_coded_time_seconds > 0 ? short_time_detailed(total_coded_time_seconds) : '0m' %> | TZ: <%= h(user.timezone) %>">
<div class="absolute border-r border-gray-700" style="left: <%= column_left %>px; width: <%= min_column_width_px %>px; top: 120px; bottom: 0; padding-bottom: 0;"></div>
<div class="absolute top-0 p-3 rounded-lg shadow-lg <%= trust_level_bg %>" data-user-id="<%= user.id %>" style="left: <%= column_left + 2 %>px; width: <%= min_column_width_px - 4 %>px;" title="User ID: <%= user.id %> - <%= user.respond_to?(:display_name) && user.display_name.present? ? h(user.display_name) : h(user.email_addresses.first&.email) %> | Total Coded: <%= total_coded_time_seconds && total_coded_time_seconds > 0 ? short_time_detailed(total_coded_time_seconds) : '0m' %> | TZ: <%= h(user.timezone) %>">
<div class="flex items-center space-x-1 mb-1">
<%= render "shared/user_mention", user: user %>
<%= render 'shared/user_mention', user: user %>
</div>
<div class="flex justify-center items-center gap-4 mb-1 text-center">
<% if current_user && user != current_user && user.slack_uid.present? %>
<div>
<%= link_to "Slack", "slack://user?team=T0266FRGM&id=#{user.slack_uid}",
target: "_blank",
class: "text-xs text-blue-400 hover:text-blue-300 underline" %>
</div>
<div>
<%= link_to 'Slack', "slack://user?team=T0266FRGM&id=#{user.slack_uid}", target: '_blank', class: 'text-xs text-blue-400 hover:text-blue-300 underline' %>
</div>
<% end %>
<% if user.respond_to?(:github_profile_url) && user.github_profile_url.present? %>
<div>
<%= link_to "Git", user.github_profile_url,
target: "_blank",
class: "text-xs text-green-400 hover:text-green-300 underline" %>
</div>
<div>
<%= link_to 'Git', user.github_profile_url, target: '_blank', class: 'text-xs text-green-400 hover:text-green-300 underline' %>
</div>
<% end %>
<div>
<span class="text-sm"><%= trust_level_emoji %></span>
<span class="text-sm"><%= trust_level_emoji %></span>
</div>
<% if current_user && current_user.admin_level.in?(["admin", "superadmin"]) && user != current_user %>
<div>
<button onclick="setTrust(<%= user.id.to_json %>)"
class="text-xs text-gray-300 hover:text-white"
title="Set trust level">🔨</button>
</div>
<div>
<button onclick="setTrust(<%= user.id.to_json %>)" class="text-xs text-gray-300 hover:text-white" title="Set trust level">🔨</button>
</div>
<% end %>
</div>
<div class="text-sm font-medium mb-1 <%= total_coded_time_seconds && total_coded_time_seconds > 0 ? 'text-green-300' : 'text-gray-400' %>">
<%= total_coded_time_seconds && total_coded_time_seconds > 0 ? "#{short_time_simple(total_coded_time_seconds)} coded" : "No time coded" %>
<%= total_coded_time_seconds && total_coded_time_seconds > 0 ? "#{short_time_simple(total_coded_time_seconds)} coded" : 'No time coded' %>
</div>
<div class="text-xs text-gray-300">
@ -216,54 +185,45 @@
end
%>
<% if show_current_time_line %>
<div class="absolute w-full h-0.5 bg-red-500 z-300 flex items-center"
style="left: <%= hour_label_width %>px; right: 0; top: <%= current_time_line_top_px %>px;">
<div class="absolute w-full h-0.5 bg-red-500 z-300 flex items-center" style="left: <%= hour_label_width %>px; right: 0; top: <%= current_time_line_top_px %>px;">
<div class="bg-red-500 text-white text-xs px-2 py-1 rounded -ml-16">NOW</div>
</div>
<% end %>
<%
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_value = span_data[:end_time] || (span_data[:start_time] + span_data[:duration])
end_time_in_zone = Time.at(end_time_value).in_time_zone(span_user_tz)
today_start_of_day_for_span_user = @date.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 = 120 + (minutes_from_view_start * pixels_per_minute).round
title_parts = []
title_parts << "Languages: #{span_data[:languages].join(', ')}" if span_data[:languages]&.any?
project_title_segments = (span_data[:projects_edited_details] || []).map do |proj_detail|
"#{proj_detail[:name]}#{proj_detail[:repo_url] ? ' (GitHub)' : ' (No Repo)'}"
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_value = span_data[:end_time] || (span_data[:start_time] + span_data[:duration])
end_time_in_zone = Time.at(end_time_value).in_time_zone(span_user_tz)
today_start_of_day_for_span_user = @date.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 = 120 + (minutes_from_view_start * pixels_per_minute).round
title_parts = []
title_parts << "Languages: #{span_data[:languages].join(', ')}" if span_data[:languages]&.any?
project_title_segments = (span_data[:projects_edited_details] || []).map { |proj_detail| "#{proj_detail[:name]}#{proj_detail[:repo_url] ? ' (GitHub)' : ' (No Repo)'}" }
title_parts << "Projects: #{project_title_segments.join('; ')}" if project_title_segments.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 += ", +#{files_to_show.length - max_files_in_tooltip} more" 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 << "Time: #{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, projects_to_display: span_data[:projects_edited_details] || [], 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
title_parts << "Projects: #{project_title_segments.join('; ')}" if project_title_segments.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 += ", +#{files_to_show.length - max_files_in_tooltip} more" 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 << "Time: #{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,
projects_to_display: span_data[:projects_edited_details] || [],
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
%>
<% users_data_array.each_with_index do |data, index| %>
@ -276,25 +236,24 @@
<% Array(user_spans).each do |span_data| %>
<% props = calculate_span_properties.call(span_data, user.timezone || primary_user_tz) %>
<% next unless props %>
<div class="absolute rounded-md p-2 text-white text-xs overflow-hidden z-10"
style="background-color: <%= block_color %>;
<div
class="absolute rounded-md p-2 text-white text-xs overflow-hidden z-10"
style="background-color: <%= block_color %>;
left: <%= column_left + 2 %>px;
width: <%= min_column_width_px - 4 %>px;
top: <%= props[:final_top_px] %>px;
height: <%= props[:height_px] %>px;"
title="<%= props[:title] %>">
title="<%= props[:title] %>">
<div class="font-medium truncate">
<% if props[:projects_to_display]&.any? %>
<% props[:projects_to_display].each_with_index do |project_detail, p_idx| %>
<% if project_detail[:repo_url].present? %>
<a href="<%= project_detail[:repo_url] %>" target="_blank" rel="noopener noreferrer"
class="text-white hover:text-gray-200 underline"
title="Open <%= h(project_detail[:name]) %> on GitHub"><%= h(project_detail[:name]).truncate(20) %></a>
<a href="<%= project_detail[:repo_url] %>" target="_blank" rel="noopener noreferrer" class="text-white hover:text-gray-200 underline" title="Open <%= h(project_detail[:name]) %> on GitHub"><%= h(project_detail[:name]).truncate(20) %></a>
<% else %>
<span title="<%= h(project_detail[:name]) %> - No GitHub Repo Mapped"><%= h(project_detail[:name]).truncate(20) %> 🚫</span>
<% end %>
<% if p_idx < props[:projects_to_display].length - 1 && props[:height_px] > 20 %>
<%= " / " %>
<%= ' / ' %>
<% end %>
<% end %>
<% else %>
@ -321,10 +280,7 @@
<% pill_left_px = column_left + 93 %>
<% commit_minutes_from_view_start = ((Time.at(commit[:timestamp]).in_time_zone(user_tz) - view_start_datetime) / 60.0).to_f %>
<% commit_top_px = 120 + (commit_minutes_from_view_start * pixels_per_minute).round %>
<a href="<%= commit[:github_url] %>" target="_blank" rel="noopener noreferrer"
title="<%= commit[:message] %>"
class="absolute z-20 bg-darker text-white rounded-full px-2 py-1 text-xs hover:bg-gray-800 transition-colors"
style="left: <%= pill_left_px %>px; top: <%= commit_top_px %>px; transform: translateX(-50%);">
<a href="<%= commit[:github_url] %>" target="_blank" rel="noopener noreferrer" title="<%= commit[:message] %>" class="absolute z-20 bg-darker text-white rounded-full px-2 py-1 text-xs hover:bg-gray-800 transition-colors" style="left: <%= pill_left_px %>px; top: <%= commit_top_px %>px; transform: translateX(-50%);">
<span class="<%= commit[:additions].to_i > 0 ? 'text-green-400 font-semibold' : '' %>">+<%= commit[:additions] %></span>
/
<span class="<%= commit[:deletions].to_i > 0 ? 'text-red-400 font-semibold' : '' %>">-<%= commit[:deletions] %></span>
@ -340,103 +296,102 @@
</div>
<% end %>
</div>
</div>
<script>
const trusts = {
'green': { emoji: '🟢', name: 'Green - Trusted', bg: 'bg-green-500/20' },
'yellow': { emoji: '🟡', name: 'Yellow - Suspected', bg: 'bg-yellow-500/20' },
'red': { emoji: '🔴', name: 'Red - Convicted (banned)', bg: 'bg-red-500/20' },
'blue': { emoji: '🔵', name: 'Blue - Unscored', bg: 'bg-blue-500/20' },
'1': { emoji: '🟢', name: 'Green - Trusted', bg: 'bg-green-500/20', level: 'green' },
'2': { emoji: '🟡', name: 'Yellow - Suspected', bg: 'bg-yellow-500/20', level: 'yellow' },
'3': { emoji: '🔴', name: 'Red - Convicted (banned)', bg: 'bg-red-500/20', level: 'red' },
'4': { emoji: '🔵', name: 'Blue - Unscored', bg: 'bg-blue-500/20', level: 'blue' }
};
const trusts = {
green: { emoji: "🟢", name: "Green - Trusted", bg: "bg-green-500/20" },
yellow: { emoji: "🟡", name: "Yellow - Suspected", bg: "bg-yellow-500/20" },
red: { emoji: "🔴", name: "Red - Convicted (banned)", bg: "bg-red-500/20" },
blue: { emoji: "🔵", name: "Blue - Unscored", bg: "bg-blue-500/20" },
1: { emoji: "🟢", name: "Green - Trusted", bg: "bg-green-500/20", level: "green" },
2: { emoji: "🟡", name: "Yellow - Suspected", bg: "bg-yellow-500/20", level: "yellow" },
3: { emoji: "🔴", name: "Red - Convicted (banned)", bg: "bg-red-500/20", level: "red" },
4: { emoji: "🔵", name: "Blue - Unscored", bg: "bg-blue-500/20", level: "blue" },
};
window.setTrust = function(userId) {
const adminLevel = document.querySelector('meta[name="current-user-admin-level"]')?.content;
const isAdmin = adminLevel === 'admin' || adminLevel === 'superadmin';
const isSuperadmin = adminLevel === 'superadmin';
// we validate this on the server, dont kill me
window.setTrust = function (userId) {
const adminLevel = document.querySelector('meta[name="current-user-admin-level"]')?.content;
const isAdmin = adminLevel === "admin" || adminLevel === "superadmin";
const isSuperadmin = adminLevel === "superadmin";
// we validate this on the server, dont kill me
if (!isAdmin) {
alert('you dont have human rights to do that');
return;
}
if (!isAdmin) {
alert("you dont have human rights to do that");
return;
}
let options = '🟢 Green (1) - Trusted\n🟡 Yellow (2) - Suspected\n🔵 Blue (4) - Unscored';
if (isSuperadmin) {
options = '🟢 Green (1) - Trusted\n🟡 Yellow (2) - Suspected\n🔴Red (3) - Convicted (banned)\n🔵 Blue (4) - Unscored';
}
let options = "🟢 Green (1) - Trusted\n🟡 Yellow (2) - Suspected\n🔵 Blue (4) - Unscored";
if (isSuperadmin) {
options = "🟢 Green (1) - Trusted\n🟡 Yellow (2) - Suspected\n🔴Red (3) - Convicted (banned)\n🔵 Blue (4) - Unscored";
}
const input = prompt(`set the trust for ${userId}\n\n${options}\n\nenter number or color`);
if (!input) return;
const input = prompt(`set the trust for ${userId}\n\n${options}\n\nenter number or color`);
if (!input) return;
const normalizedInput = input.toLowerCase().trim();
const trust = trusts[normalizedInput];
const normalizedInput = input.toLowerCase().trim();
const trust = trusts[normalizedInput];
if (!trust) {
alert('read the popup idiot');
return;
}
if (!trust) {
alert("read the popup idiot");
return;
}
const levelForAPI = trust.level || normalizedInput;
const levelForAPI = trust.level || normalizedInput;
if (levelForAPI === 'red' && !isSuperadmin) {
alert('nice try neon');
return;
}
if (levelForAPI === "red" && !isSuperadmin) {
alert("nice try neon");
return;
}
const reason = prompt('please explain why you are doing this to this poor soul');
if (!reason || reason.trim() === '') {
alert('you gotta put something down silly');
return;
}
const reason = prompt("please explain why you are doing this to this poor soul");
if (!reason || reason.trim() === "") {
alert("you gotta put something down silly");
return;
}
const notes = prompt('anything else you wanna add? (optional)');
const notes = prompt("anything else you wanna add? (optional)");
fetch(`/users/${userId}/update_trust_level`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
trust_level: levelForAPI,
reason: reason.trim(),
notes: notes ? notes.trim() : ''
fetch(`/users/${userId}/update_trust_level`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify({
trust_level: levelForAPI,
reason: reason.trim(),
notes: notes ? notes.trim() : "",
}),
})
})
.then(response => {
if (!response.ok) {
return response.json().then(err => {
throw new Error(err.error || `${response.status} ${response.statusText}`);
});
}
return response.json();
})
.then(data => {
const card = Array.from(document.querySelectorAll('[data-user-id]')).find(card => {
const title = card.getAttribute('title') || '';
return title.match(new RegExp(`User ID:\\s*${userId}\\b`));
// what the fuck is this
});
if (card) {
card.classList.remove('bg-red-500/20', 'bg-green-500/20', 'bg-yellow-500/20', 'bg-blue-500/20', 'bg-gray-500/20');
card.classList.add(trust.bg);
const span = card.querySelector('span.text-sm');
if (span) {
span.textContent = trust.emoji;
}
}
.then((response) => {
if (!response.ok) {
return response.json().then((err) => {
throw new Error(err.error || `${response.status} ${response.statusText}`);
});
}
return response.json();
})
.then((data) => {
const card = Array.from(document.querySelectorAll("[data-user-id]")).find((card) => {
const title = card.getAttribute("title") || "";
return title.match(new RegExp(`User ID:\\s*${userId}\\b`));
// what the fuck is this
});
if (card) {
card.classList.remove("bg-red-500/20", "bg-green-500/20", "bg-yellow-500/20", "bg-blue-500/20", "bg-gray-500/20");
card.classList.add(trust.bg);
const span = card.querySelector("span.text-sm");
if (span) {
span.textContent = trust.emoji;
}
}
alert(`set trust to ${trust.name}\nreason: ${reason}${notes ? '\nanything else? ' + notes : ''}`);
})
.catch(error => {
console.error(error);
alert('shit ' + error.message);
});
};
alert(`set trust to ${trust.name}\nreason: ${reason}${notes ? "\nanything else? " + notes : ""}`);
})
.catch((error) => {
console.error(error);
alert("shit " + error.message);
});
};
</script>

View file

@ -1,4 +1,4 @@
<% content_for :title, "admin aboose logs" %>
<% content_for :title, 'admin aboose logs' %>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-8">
@ -10,42 +10,25 @@
<%= form_with url: admin_trust_level_audit_logs_path, method: :get, local: true, class: "space-y-4" do |form| %>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<%= form.label :user_search, "user lookup", class: "block text-sm font-medium text-gray-300 mb-2" %>
<%= form.text_field :user_search,
value: @user_search,
placeholder: "just put anything here or something",
class: "w-full px-3 py-2 bg-darkless rounded-md text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" %>
<%= form.label :user_search, 'user lookup', class: 'block text-sm font-medium text-gray-300 mb-2' %>
<%= form.text_field :user_search, value: @user_search, placeholder: 'just put anything here or something', class: 'w-full px-3 py-2 bg-darkless rounded-md text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent' %>
</div>
<div>
<%= form.label :admin_search, "admin lookup", class: "block text-sm font-medium text-gray-300 mb-2" %>
<%= form.text_field :admin_search,
value: @admin_search,
placeholder: "just put anything here or something",
class: "w-full px-3 py-2 bg-darkless rounded-md text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" %>
<%= form.label :admin_search, 'admin lookup', class: 'block text-sm font-medium text-gray-300 mb-2' %>
<%= form.text_field :admin_search, value: @admin_search, placeholder: 'just put anything here or something', class: 'w-full px-3 py-2 bg-darkless rounded-md text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent' %>
</div>
<div>
<%= form.label :trust_level_filter, "filter by trust updates", class: "block text-sm font-medium text-gray-300 mb-2" %>
<%= form.select :trust_level_filter,
options_for_select([
['All Changes', 'all'],
['🔴 Set to Convicted', 'to_convicted'],
['🟢 Set to Trusted', 'to_trusted'],
['🟡 Set to Suspected', 'to_suspected'],
['🔵 Set to Unscored', 'to_unscored']
], @trust_level_filter || 'all'),
{},
{ class: "w-full px-3 py-2 bg-darkless rounded-md text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" } %>
<%= form.label :trust_level_filter, 'filter by trust updates', class: 'block text-sm font-medium text-gray-300 mb-2' %>
<%= form.select :trust_level_filter, options_for_select([['All Changes', 'all'], ['🔴 Set to Convicted', 'to_convicted'], ['🟢 Set to Trusted', 'to_trusted'], ['🟡 Set to Suspected', 'to_suspected'], ['🔵 Set to Unscored', 'to_unscored']], @trust_level_filter || 'all'), {}, { class: 'w-full px-3 py-2 bg-darkless rounded-md text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent' } %>
</div>
</div>
<div class="flex items-center gap-4">
<%= form.submit "run that shit", class: "bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md font-medium transition-colors" %>
<%= link_to "alt+f4", admin_trust_level_audit_logs_path, class: "bg-gray-600 hover:bg-darkless text-white px-4 py-2 rounded-md font-medium transition-colors" %>
<span class="text-md text-gray-400">
found <%= @audit_logs.length %> result<%= @audit_logs.length == 1 ? '' : 's' %>
</span>
<%= form.submit 'run that shit', class: 'bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md font-medium transition-colors' %>
<%= link_to 'alt+f4', admin_trust_level_audit_logs_path, class: 'bg-gray-600 hover:bg-darkless text-white px-4 py-2 rounded-md font-medium transition-colors' %>
<span class="text-md text-gray-400"> found <%= @audit_logs.length %> result<%= @audit_logs.length == 1 ? '' : 's' %> </span>
</div>
<% end %>
</div>
@ -65,9 +48,7 @@
</p>
<% end %>
</div>
<%= link_to "fuckin abort",
admin_trust_level_audit_logs_path,
class: "text-sm bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded" %>
<%= link_to 'fuckin abort', admin_trust_level_audit_logs_path, class: 'text-sm bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded' %>
</div>
</div>
<% end %>
@ -77,31 +58,19 @@
<table class="min-w-full">
<thead class="">
<tr>
<th class="px-6 py-3 text-left text-sm font-medium text-gray-300">
time
</th>
<th class="px-6 py-3 text-left text-sm font-medium text-gray-300">
goober
</th>
<th class="px-6 py-3 text-left text-sm font-medium text-gray-300">
change
</th>
<th class="px-6 py-3 text-left text-sm font-medium text-gray-300">
goobed by
</th>
<th class="px-6 py-3 text-left text-sm font-medium text-gray-300">
why
</th>
<th class="px-6 py-3 text-left text-sm font-medium text-gray-300">
link
</th>
<th class="px-6 py-3 text-left text-sm font-medium text-gray-300">time</th>
<th class="px-6 py-3 text-left text-sm font-medium text-gray-300">goober</th>
<th class="px-6 py-3 text-left text-sm font-medium text-gray-300">change</th>
<th class="px-6 py-3 text-left text-sm font-medium text-gray-300">goobed by</th>
<th class="px-6 py-3 text-left text-sm font-medium text-gray-300">why</th>
<th class="px-6 py-3 text-left text-sm font-medium text-gray-300">link</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-950">
<% @audit_logs.each do |log| %>
<tr class="hover:bg-darkless">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
<%= log.created_at.strftime("%b %d, %Y at %I:%M %p") %>
<%= log.created_at.strftime('%b %d, %Y at %I:%M %p') %>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
@ -112,9 +81,7 @@
<div class="text-sm font-medium text-white">
<%= log.user.display_name %>
</div>
<div class="text-sm text-gray-400">
ID: <%= log.user.id %>
</div>
<div class="text-sm text-gray-400">ID: <%= log.user.id %></div>
</div>
</div>
</td>
@ -122,19 +89,29 @@
<div class="flex items-center">
<span class="text-sm text-gray-300">
<%
previous_emoji = case log.previous_trust_level
when "blue" then "🔵"
when "red" then "🔴"
when "green" then "🟢"
when "yellow" then "🟡"
end
previous_emoji =
case log.previous_trust_level
when 'blue'
'🔵'
when 'red'
'🔴'
when 'green'
'🟢'
when 'yellow'
'🟡'
end
new_emoji = case log.new_trust_level
when "blue" then "🔵"
when "red" then "🔴"
when "green" then "🟢"
when "yellow" then "🟡"
end
new_emoji =
case log.new_trust_level
when 'blue'
'🔵'
when 'red'
'🔴'
when 'green'
'🟢'
when 'yellow'
'🟡'
end
%>
<%= previous_emoji %> <strong><%= log.previous_trust_level.capitalize %></strong>
@ -151,17 +128,11 @@
<div class="text-sm text-white">
<%= log.changed_by.display_name %>
<% if log.changed_by.admin_level == "superadmin" %>
<span class="inline-flex items-center px-1.5 py-0.2 rounded text-sm font-medium border border-red-950 text-red-500">
supa admin
</span>
<span class="inline-flex items-center px-1.5 py-0.2 rounded text-sm font-medium border border-red-950 text-red-500"> supa admin </span>
<% elsif log.changed_by.admin_level == "admin" %>
<span class="inline-flex items-center px-1.5 py-0.2 rounded text-sm font-medium border border-yellow-950 text-yellow-500">
admin
</span>
<span class="inline-flex items-center px-1.5 py-0.2 rounded text-sm font-medium border border-yellow-950 text-yellow-500"> admin </span>
<% elsif log.changed_by.admin_level == "viewer" %>
<span class="inline-flex items-center px-1.5 py-0.2 rounded text-sm font-medium border border-blue-950 text-blue-500">
viewer
</span>
<span class="inline-flex items-center px-1.5 py-0.2 rounded text-sm font-medium border border-blue-950 text-blue-500"> viewer </span>
<% end %>
</div>
</div>
@ -177,9 +148,7 @@
<% end %>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<%= link_to "the deets",
admin_trust_level_audit_log_path(log),
class: "text-blue-400 hover:text-blue-300" %>
<%= link_to 'the deets', admin_trust_level_audit_log_path(log), class: 'text-blue-400 hover:text-blue-300' %>
</td>
</tr>
<% end %>

View file

@ -4,9 +4,7 @@
<div class="mb-8">
<div class="flex items-center justify-between">
<h1 class="text-3xl font-bold text-white">looking at a single audit log</h1>
<%= link_to "get me outta here",
admin_trust_level_audit_logs_path,
class: "bg-darkless text-white px-4 py-2 rounded-lg" %>
<%= link_to "get me outta here", admin_trust_level_audit_logs_path, class: "bg-darkless text-white px-4 py-2 rounded-lg" %>
</div>
</div>
@ -17,15 +15,11 @@
<div class="space-y-3">
<div class="flex items-center justify-between">
<%= render "shared/user_mention", user: @audit_log.user %>
<div class="text-sm text-gray-400">
id: <%= @audit_log.user.id %>
</div>
<div class="text-sm text-gray-400">id: <%= @audit_log.user.id %></div>
</div>
<div class="pt-4">
<%= link_to "actions on this goober",
admin_trust_level_audit_logs_path(user_id: @audit_log.user.id),
class: "text-blue-400 hover:text-blue-300" %>
<%= link_to "actions on this goober", admin_trust_level_audit_logs_path(user_id: @audit_log.user.id), class: "text-blue-400 hover:text-blue-300" %>
</div>
</div>
</div>
@ -37,28 +31,18 @@
<div class="flex items-center">
<%= render "shared/user_mention", user: @audit_log.changed_by %>
<% if @audit_log.changed_by.admin_level == "superadmin" %>
<span class="inline-flex items-center px-1.5 py-0.2 rounded text-sm font-medium border border-red-950 text-red-500">
supa admin
</span>
<span class="inline-flex items-center px-1.5 py-0.2 rounded text-sm font-medium border border-red-950 text-red-500">supa admin</span>
<% elsif @audit_log.changed_by.admin_level == "admin" %>
<span class="inline-flex items-center px-1.5 py-0.2 rounded text-sm font-medium border border-yellow-950 text-yellow-500 ml-2">
admin
</span>
<span class="inline-flex items-center px-1.5 py-0.2 rounded text-sm font-medium border border-yellow-950 text-yellow-500 ml-2">admin</span>
<% elsif @audit_log.changed_by.admin_level == "viewer" %>
<span class="inline-flex items-center px-1.5 py-0.2 rounded text-sm font-medium border border-blue-950 text-blue-500 ml-2">
viewer
</span>
<span class="inline-flex items-center px-1.5 py-0.2 rounded text-sm font-medium border border-blue-950 text-blue-500 ml-2">viewer</span>
<% end %>
</div>
<div class="text-sm text-gray-400">
id: <%= @audit_log.changed_by.id %>
</div>
<div class="text-sm text-gray-400">id: <%= @audit_log.changed_by.id %></div>
</div>
<div class="pt-4">
<%= link_to "changes by this goober",
admin_trust_level_audit_logs_path(admin_id: @audit_log.changed_by.id),
class: "text-blue-400 hover:text-blue-300" %>
<%= link_to 'changes by this goober', admin_trust_level_audit_logs_path(admin_id: @audit_log.changed_by.id), class: 'text-blue-400 hover:text-blue-300' %>
</div>
</div>
</div>
@ -71,7 +55,7 @@
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">executed at</label>
<div class="text-white">
<%= @audit_log.created_at.strftime("%B %d, %Y at %I:%M %p %Z") %>
<%= @audit_log.created_at.strftime('%B %d, %Y at %I:%M %p %Z') %>
</div>
</div>
@ -79,12 +63,17 @@
<label class="block text-sm font-medium text-gray-400 mb-2">before</label>
<div class="text-white text-lg">
<%
a = case @audit_log.previous_trust_level
when "blue" then "🔵"
when "red" then "🔴"
when "green" then "🟢"
when "yellow" then "🟡"
end
a =
case @audit_log.previous_trust_level
when 'blue'
'🔵'
when 'red'
'🔴'
when 'green'
'🟢'
when 'yellow'
'🟡'
end
%>
<%= a %> <strong><%= @audit_log.previous_trust_level.capitalize %></strong>
</div>
@ -94,12 +83,17 @@
<label class="block text-sm font-medium text-gray-400 mb-2">after</label>
<div class="text-white text-lg">
<%
b = case @audit_log.new_trust_level
when "blue" then "🔵"
when "red" then "🔴"
when "green" then "🟢"
when "yellow" then "🟡"
end
b =
case @audit_log.new_trust_level
when 'blue'
'🔵'
when 'red'
'🔴'
when 'green'
'🟢'
when 'yellow'
'🟡'
end
%>
<%= b %> <strong><%= @audit_log.new_trust_level.capitalize %></strong>
</div>

View file

@ -1,21 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type'>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
</head>
<body>
<h1>Welcome back to Hackatime!</h1>
<p>
Click the link below to sign in to your account:
</p>
<p>Click the link below to sign in to your account:</p>
<p>
<%= link_to 'Sign in to Hackatime', auth_token_url(@token.token) %>
</p>
<p>
This link will expire in 30 minutes and can only be used once.
</p>
<p>
If you didn't request this email, you can safely ignore it.
</p>
<p>This link will expire in 30 minutes and can only be used once.</p>
<p>If you didn't request this email, you can safely ignore it.</p>
</body>
</html>

View file

@ -15,7 +15,8 @@
<div class="space-y-4">
<div class="flex items-center justify-between">
<span class="text-gray-300">Status</span>
<span class="px-3 py-1 rounded-full text-sm font-medium
<span
class="px-3 py-1 rounded-full text-sm font-medium
<%= @deletion_request.approved? ? 'bg-green-600/20 text-green-400' : 'bg-yellow-600/20 text-yellow-400' %>">
<%= @deletion_request.status.humanize %>
</span>
@ -23,29 +24,24 @@
<div class="flex items-center justify-between ">
<span class="text-gray-300">Requested</span>
<span class="text-white"><%= @deletion_request.requested_at.strftime("%B %d, %Y at %I:%M %p") %></span>
<span class="text-white"><%= @deletion_request.requested_at.strftime('%B %d, %Y at %I:%M %p') %></span>
</div>
<% if @deletion_request.pending? %>
<div class="p-4 bg-yellow-900/30 border border-yellow-700 rounded">
<p class="text-yellow-200">
Your deletion request is pending approval. During this time, we will review your request and get back to you as soon as possible.
</p>
<p class="text-yellow-200">Your deletion request is pending approval. During this time, we will review your request and get back to you as soon as possible.</p>
</div>
<% elsif @deletion_request.approved? %>
<div class="flex items-center justify-between">
<span class="text-gray-300">Deletion Date</span>
<span class="text-red-400 font-semibold">
<%= @deletion_request.scheduled_deletion_at.strftime("%B %d, %Y") %>
<%= @deletion_request.scheduled_deletion_at.strftime('%B %d, %Y') %>
(<%= @deletion_request.days_until_deletion %> days remaining)
</span>
</div>
<div class="p-4 bg-red-900/30 border border-red-700 rounded">
<p class="text-red-200">
Your account will be permanently deleted on <%= @deletion_request.scheduled_deletion_at.strftime("%B %d, %Y") %>.
After deletion, your email address will be retained on file, but all other personal information will be removed or anonymized.
</p>
<p class="text-red-200">Your account will be permanently deleted on <%= @deletion_request.scheduled_deletion_at.strftime('%B %d, %Y') %>. After deletion, your email address will be retained on file, but all other personal information will be removed or anonymized.</p>
</div>
<% end %>
@ -63,11 +59,7 @@
<% end %>
<% if @deletion_request.can_be_cancelled? %>
<%= button_to "I changed my mind",
cancel_deletion_path,
method: :delete,
form: { class: "w-full" },
class: "w-full px-4 py-3 bg-primary hover:bg-red-600 text-white font-medium rounded text-center transition-colors duration-200 cursor-pointer" %>
<%= button_to "I changed my mind", cancel_deletion_path, method: :delete, form: { class: "w-full" }, class: "w-full px-4 py-3 bg-primary hover:bg-red-600 text-white font-medium rounded text-center transition-colors duration-200 cursor-pointer" %>
<% else %>
<div class="w-full"></div>
<% end %>

View file

@ -3,26 +3,15 @@
<div class="min-h-screen text-white">
<div class="max-w-6xl mx-auto px-6 py-8">
<h1 class="text-4xl font-bold mb-4">
Welcome to <span class="text-primary">Hackatime</span>
</h1>
<p class="text-secondary text-lg mb-8">
Tracks your coding time - made by <a href="https://hackclub.com" target="_blank" class="text-primary hover:text-red underline">Hack Club</a>
</p>
<h1 class="text-4xl font-bold mb-4">Welcome to <span class="text-primary">Hackatime</span></h1>
<p class="text-secondary text-lg mb-8">Tracks your coding time - made by <a href="https://hackclub.com" target="_blank" class="text-primary hover:text-red underline">Hack Club</a></p>
<div class="bg-dark rounded-lg p-6 mb-8">
<p class="mb-4">
Hackatime is a free tool from Hack Club. It shows you how much time you spend coding. You can see what other Hack Clubbers are building too!
</p>
<p class="mb-4">Hackatime is a free tool from Hack Club. It shows you how much time you spend coding. You can see what other Hack Clubbers are building too!</p>
<p class="mb-4">
<strong class="text-primary">Why we made this:</strong> The more time you spend making things, the better you get at building cool projects!
</p>
<p class="mb-4"><strong class="text-primary">Why we made this:</strong> The more time you spend making things, the better you get at building cool projects!</p>
<p>
Hackatime is totally free. Anyone can see the <a href="https://github.com/hackclub/hackatime" target="_blank" class="text-primary hover:text-red underline">code</a>. It's like <a href="https://wakatime.com" target="_blank" class="text-primary hover:text-red underline">WakaTime</a> but free and open source.
</p>
<p>Hackatime is totally free. Anyone can see the <a href="https://github.com/hackclub/hackatime" target="_blank" class="text-primary hover:text-red underline">code</a>. It's like <a href="https://wakatime.com" target="_blank" class="text-primary hover:text-red underline">WakaTime</a> but free and open source.</p>
</div>
<h3 class="text-2xl font-semibold text-primary mb-4">🎯 How to Start</h3>
@ -35,15 +24,11 @@
</ol>
</div>
<div class="bg-dark border-l-4 border-primary rounded-r-lg p-6 mb-8">
<strong class="text-primary">💡 Tip:</strong> The <a href="<%= my_wakatime_setup_path %>" class="text-primary hover:text-red underline">setup page</a> does everything for you. No hard stuff to figure out!
</div>
<div class="bg-dark border-l-4 border-primary rounded-r-lg p-6 mb-8"><strong class="text-primary">💡 Tip:</strong> The <a href="<%= my_wakatime_setup_path %>" class="text-primary hover:text-red underline">setup page</a> does everything for you. No hard stuff to figure out!</div>
<h3 class="text-2xl font-semibold text-primary mb-4">🔌 What Code Editors Work</h3>
<div class="bg-dark rounded-lg p-6 mb-8">
<p class="mb-6">
Hackatime works with <strong class="text-white">any editor</strong> that has <a href="https://wakatime.com" target="_blank" class="text-primary hover:text-red underline">WakaTime</a>. Just add the WakaTime plugin to your editor:
</p>
<p class="mb-6">Hackatime works with <strong class="text-white">any editor</strong> that has <a href="https://wakatime.com" target="_blank" class="text-primary hover:text-red underline">WakaTime</a>. Just add the WakaTime plugin to your editor:</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-darkless rounded-lg p-4">
@ -106,66 +91,32 @@
<strong class="text-primary">Not seeing your time?</strong> Go to the <a href="<%= my_wakatime_setup_path %>" class="text-primary hover:text-red underline">setup page</a>
to check if everything is working.
</p>
<p>
<strong class="text-primary">Have questions?</strong> Ask in the <a href="https://hackclub.slack.com" target="_blank" class="text-primary hover:text-red underline">Hack Club Slack</a>
(#hackatime-v2 channel) or <a href="https://github.com/hackclub/hackatime/issues" target="_blank" class="text-primary hover:text-red underline">ask on GitHub</a>.
</p>
<p><strong class="text-primary">Have questions?</strong> Ask in the <a href="https://hackclub.slack.com" target="_blank" class="text-primary hover:text-red underline">Hack Club Slack</a> (#hackatime-v2 channel) or <a href="https://github.com/hackclub/hackatime/issues" target="_blank" class="text-primary hover:text-red underline">ask on GitHub</a>.</p>
</div>
<h3 class="text-2xl font-semibold text-primary mb-4">🔧 Supported Editors</h3>
<div class="bg-dark rounded-lg p-6 mb-8">
<p class="mb-6">
Hackatime works with any editor that supports WakaTime. Click on your editor below for setup instructions:
</p>
<p class="mb-6">Hackatime works with any editor that supports WakaTime. Click on your editor below for setup instructions:</p>
<div id="supported-editors" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
<!-- Most popular editors among teenagers, ordered by popularity -->
<% popular_editors = [
['VS Code', 'vs-code'], ['PyCharm', 'pycharm'], ['IntelliJ IDEA', 'intellij-idea'],
['Sublime Text', 'sublime-text'], ['Vim', 'vim'], ['Neovim', 'neovim'],
['Android Studio', 'android-studio'], ['Xcode', 'xcode'], ['Unity', 'unity'],
['Godot', 'godot'], ['Cursor', 'cursor'], ['Zed', 'zed'],
['Terminal', 'terminal'], ['WebStorm', 'webstorm'], ['Eclipse', 'eclipse'],
['Emacs', 'emacs'], ['Jupyter', 'jupyter'], ['OnShape', 'onshape']
] %>
<% popular_editors = [['VS Code', 'vs-code'], %w[PyCharm pycharm], ['IntelliJ IDEA', 'intellij-idea'], ['Sublime Text', 'sublime-text'], %w[Vim vim], %w[Neovim neovim], ['Android Studio', 'android-studio'], %w[Xcode xcode], %w[Unity unity], %w[Godot godot], %w[Cursor cursor], %w[Zed zed], %w[Terminal terminal], %w[WebStorm webstorm], %w[Eclipse eclipse], %w[Emacs emacs], %w[Jupyter jupyter], %w[OnShape onshape]] %>
<% popular_editors.each do |name, slug| %>
<a href="<%= doc_path("editors/#{slug}") %>" class="bg-darkless rounded-lg p-3 hover:bg-primary/75 transition-colors text-center block">
<img src="/images/editor-icons/<%= slug %>-128.png" alt="<%= name %>" class="w-12 h-12 mx-auto mb-2">
<div class="text-sm text-white"><%= name %></div>
</a>
<% end %>
</div>
</div>
<details class="bg-dark rounded-lg p-6 mb-8">
<summary class="cursor-pointer font-semibold text-primary">View all 80 supported editors</summary>
<div id="all-editors" class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-3 mt-4 p-4">
<%
# All 80 editors - alphabetically sorted
all_editors = [
['Android Studio', 'android-studio'], ['AppCode', 'appcode'], ['Aptana', 'aptana'], ['Arduino IDE', 'arduino-ide'],
['Azure Data Studio', 'azure-data-studio'], ['Blender', 'blender'], ['Brackets', 'brackets'], ['Brave', 'brave'],
['C++ Builder', 'c++-builder'], ['Canva', 'canva'], ['Chrome', 'chrome'], ['CLion', 'clion'],
['Cloud9', 'cloud9'], ['Coda', 'coda'], ['CodeTasty', 'codetasty'], ['Cursor', 'cursor'],
['DataGrip', 'datagrip'], ['DataSpell', 'dataspell'], ['DBeaver', 'dbeaver'], ['Delphi', 'delphi'],
['Discord', 'discord'], ['Eclipse', 'eclipse'], ['Edge', 'edge'], ['Emacs', 'emacs'],
['Eric', 'eric'], ['Excel', 'excel'], ['Figma', 'figma'], ['Firefox', 'firefox'],
['Gedit', 'gedit'], ['Godot', 'godot'], ['GoLand', 'goland'], ['HBuilder X', 'hbuilder-x'], ['IDA Pro', 'ida-pro'],
['IntelliJ IDEA', 'intellij-idea'], ['Jupyter', 'jupyter'], ['Kakoune', 'kakoune'], ['Kate', 'kate'],
['Komodo', 'komodo'], ['Micro', 'micro'], ['MPS', 'mps'], ['Neovim', 'neovim'],
['NetBeans', 'netbeans'], ['Notepad++', 'notepad++'], ['Nova', 'nova'], ['Obsidian', 'obsidian'],
['OnShape', 'onshape'], ['Oxygen', 'oxygen'], ['PhpStorm', 'phpstorm'], ['Postman', 'postman'], ['PowerPoint', 'powerpoint'],
['Processing', 'processing'], ['Pulsar', 'pulsar'], ['PyCharm', 'pycharm'], ['ReClassEx', 'reclassex'],
['Rider', 'rider'], ['Roblox Studio', 'roblox-studio'], ['RubyMine', 'rubymine'], ['RustRover', 'rustrover'],
['Safari', 'safari'], ['SiYuan', 'siyuan'], ['Sketch', 'sketch'], ['SlickEdit', 'slickedit'],
['SQL Server Management Studio', 'sql-server-management-studio'], ['Sublime Text', 'sublime-text'],
['Terminal', 'terminal'], ['TeXstudio', 'texstudio'], ['TextMate', 'textmate'], ['Trae', 'trae'],
['Unity', 'unity'], ['Unreal Engine 4', 'unreal-engine-4'], ['Vim', 'vim'], ['Visual Studio', 'visual-studio'], ['VS Code', 'vs-code'],
['WebStorm', 'webstorm'], ['Windsurf', 'windsurf'], ['Wing', 'wing'], ['Word', 'word'],
['Xcode', 'xcode'], ['Zed', 'zed'], ['Swift Playgrounds', 'swift-playgrounds']
].sort_by { |editor| editor[0] }
%>
<%
# All 80 editors - alphabetically sorted
all_editors = [['Android Studio', 'android-studio'], %w[AppCode appcode], %w[Aptana aptana], ['Arduino IDE', 'arduino-ide'], ['Azure Data Studio', 'azure-data-studio'], %w[Blender blender], %w[Brackets brackets], %w[Brave brave], ['C++ Builder', 'c++-builder'], %w[Canva canva], %w[Chrome chrome], %w[CLion clion], %w[Cloud9 cloud9], %w[Coda coda], %w[CodeTasty codetasty], %w[Cursor cursor], %w[DataGrip datagrip], %w[DataSpell dataspell], %w[DBeaver dbeaver], %w[Delphi delphi], %w[Discord discord], %w[Eclipse eclipse], %w[Edge edge], %w[Emacs emacs], %w[Eric eric], %w[Excel excel], %w[Figma figma], %w[Firefox firefox], %w[Gedit gedit], %w[Godot godot], %w[GoLand goland], ['HBuilder X', 'hbuilder-x'], ['IDA Pro', 'ida-pro'], ['IntelliJ IDEA', 'intellij-idea'], %w[Jupyter jupyter], %w[Kakoune kakoune], %w[Kate kate], %w[Komodo komodo], %w[Micro micro], %w[MPS mps], %w[Neovim neovim], %w[NetBeans netbeans], %w[Notepad++ notepad++], %w[Nova nova], %w[Obsidian obsidian], %w[OnShape onshape], %w[Oxygen oxygen], %w[PhpStorm phpstorm], %w[Postman postman], %w[PowerPoint powerpoint], %w[Processing processing], %w[Pulsar pulsar], %w[PyCharm pycharm], %w[ReClassEx reclassex], %w[Rider rider], ['Roblox Studio', 'roblox-studio'], %w[RubyMine rubymine], %w[RustRover rustrover], %w[Safari safari], %w[SiYuan siyuan], %w[Sketch sketch], %w[SlickEdit slickedit], ['SQL Server Management Studio', 'sql-server-management-studio'], ['Sublime Text', 'sublime-text'], %w[Terminal terminal], %w[TeXstudio texstudio], %w[TextMate textmate], %w[Trae trae], %w[Unity unity], ['Unreal Engine 4', 'unreal-engine-4'], %w[Vim vim], ['Visual Studio', 'visual-studio'], ['VS Code', 'vs-code'], %w[WebStorm webstorm], %w[Windsurf windsurf], %w[Wing wing], %w[Word word], %w[Xcode xcode], %w[Zed zed], ['Swift Playgrounds', 'swift-playgrounds']].sort_by { |editor| editor[0] }
%>
<% all_editors.each do |name, slug| %>
<a href="<%= doc_path("editors/#{slug}") %>" class="bg-darkless rounded p-2 hover:bg-primary/75 transition-colors text-center block">
<img src="/images/editor-icons/<%= slug %>-128.png" alt="<%= name %>" class="w-8 h-8 mx-auto mb-1">
@ -180,9 +131,7 @@
<div class="text-center">
<p class="text-secondary">
Found an issue with the docs?
<a href="https://github.com/hackclub/hackatime" target="_blank" class="text-primary hover:text-red underline">
Edit on GitHub
</a> - we'd love your help making them better!
<a href="https://github.com/hackclub/hackatime" target="_blank" class="text-primary hover:text-red underline"> Edit on GitHub </a> - we'd love your help making them better!
</p>
</div>
</div>

View file

@ -44,7 +44,6 @@
}
.prose a:hover {
color: var(--color-red) !important;
}
</style>
<% end %>
@ -65,7 +64,8 @@
<% end %>
<% end %>
</nav>
<div class="bg-dark rounded-lg p-8 mb-8 prose prose-invert prose-lg max-w-none
<div
class="bg-dark rounded-lg p-8 mb-8 prose prose-invert prose-lg max-w-none
prose-headings:text-primary prose-headings:font-bold prose-headings:leading-tight
prose-h1:text-4xl prose-h1:mb-6 prose-h1:text-primary prose-h1:mt-0
prose-h2:text-2xl prose-h2:mt-10 prose-h2:mb-4 prose-h2:text-primary prose-h2:border-b prose-h2:border-primary/20 prose-h2:pb-2

View file

@ -1,18 +1,14 @@
<%- submit_btn_css ||= 'default' %>
<%= form_tag oauth_application_path(application), method: :delete, class: submit_btn_css == 'danger' ? 'w-full' : 'inline' do %>
<% if submit_btn_css == 'danger' %>
<button type="submit"
onclick="return confirm(<%= t('doorkeeper.applications.confirmations.destroy').to_json %>)"
class="w-full inline-flex items-center justify-center gap-2 px-4 py-2 bg-red hover:opacity-90 text-white font-medium rounded transition-colors duration-200">
<button type="submit" onclick="return confirm(<%= t('doorkeeper.applications.confirmations.destroy').to_json %>)" class="w-full inline-flex items-center justify-center gap-2 px-4 py-2 bg-red hover:opacity-90 text-white font-medium rounded transition-colors duration-200">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
<%= t('doorkeeper.applications.buttons.destroy') %>
</button>
<% else %>
<button type="submit"
onclick="return confirm(<%= t('doorkeeper.applications.confirmations.destroy').to_json %>)"
class="inline-flex items-center gap-1.5 px-3 py-2 bg-red hover:opacity-90 text-white text-sm font-medium rounded transition-colors duration-200">
<button type="submit" onclick="return confirm(<%= t('doorkeeper.applications.confirmations.destroy').to_json %>)" class="inline-flex items-center gap-1.5 px-3 py-2 bg-red hover:opacity-90 text-white text-sm font-medium rounded transition-colors duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>

View file

@ -29,9 +29,7 @@
<div>
<%= f.label :name, class: "block text-sm font-medium text-white mb-2" %>
<% if application.persisted? && application.verified? %>
<%= f.text_field :name,
class: "w-full px-3 py-2 bg-darkless/50 border border-darkless rounded text-secondary cursor-not-allowed",
disabled: true %>
<%= f.text_field :name, class: "w-full px-3 py-2 bg-darkless/50 border border-darkless rounded text-secondary cursor-not-allowed", disabled: true %>
<%= f.hidden_field :name %>
<p class="mt-2 text-xs text-yellow flex items-center gap-1.5">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
@ -40,10 +38,7 @@
Name is locked for verified applications. Contact a super admin to change it.
</p>
<% else %>
<%= f.text_field :name,
class: "w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary placeholder-secondary",
placeholder: "My Awesome App",
required: true %>
<%= f.text_field :name, class: "w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary placeholder-secondary", placeholder: "My Awesome App", required: true %>
<% end %>
<% if application.errors[:name].present? %>
<p class="mt-1 text-xs text-red"><%= application.errors[:name].to_sentence %></p>
@ -52,10 +47,7 @@
<div>
<%= f.label :redirect_uri, "Redirect URIs", class: "block text-sm font-medium text-white mb-2" %>
<%= f.text_area :redirect_uri,
class: "w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary placeholder-secondary font-mono text-sm",
placeholder: "https://example.com/auth/callback",
rows: 3 %>
<%= f.text_area :redirect_uri, class: "w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary placeholder-secondary font-mono text-sm", placeholder: "https://example.com/auth/callback", rows: 3 %>
<% if application.errors[:redirect_uri].present? %>
<p class="mt-1 text-xs text-red"><%= application.errors[:redirect_uri].to_sentence %></p>
<% end %>
@ -71,9 +63,7 @@
<div>
<%= f.label :scopes, class: "block text-sm font-medium text-white mb-2" %>
<%= f.text_field :scopes,
class: "w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary placeholder-secondary font-mono text-sm",
placeholder: "profile read" %>
<%= f.text_field :scopes, class: "w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary placeholder-secondary font-mono text-sm", placeholder: "profile read" %>
<% if application.errors[:scopes].present? %>
<p class="mt-1 text-xs text-red"><%= application.errors[:scopes].to_sentence %></p>
<% end %>
@ -83,8 +73,7 @@
</div>
<div class="flex items-start gap-3 p-4 bg-darkless border border-darkless rounded">
<%= f.check_box :confidential,
class: "mt-0.5 w-4 h-4 text-primary border-darkless rounded focus:ring-primary bg-darker" %>
<%= f.check_box :confidential, class: "mt-0.5 w-4 h-4 text-primary border-darkless rounded focus:ring-primary bg-darker" %>
<div>
<%= f.label :confidential, "Confidential Application", class: "text-sm font-medium text-white" %>
<p class="mt-1 text-xs text-secondary">
@ -96,9 +85,7 @@
</div>
<div class="flex items-center gap-3">
<%= f.submit t('doorkeeper.applications.buttons.submit'),
class: "px-6 py-2 bg-primary text-white font-medium rounded transition-colors duration-200 hover:opacity-90 cursor-pointer" %>
<%= link_to t('doorkeeper.applications.buttons.cancel'), oauth_applications_path,
class: "px-6 py-2 border border-darkless text-white font-medium rounded transition-colors duration-200 hover:bg-darkless" %>
<%= f.submit t('doorkeeper.applications.buttons.submit'), class: "px-6 py-2 bg-primary text-white font-medium rounded transition-colors duration-200 hover:opacity-90 cursor-pointer" %>
<%= link_to t('doorkeeper.applications.buttons.cancel'), oauth_applications_path, class: "px-6 py-2 border border-darkless text-white font-medium rounded transition-colors duration-200 hover:bg-darkless" %>
</div>
<% end %>

View file

@ -31,8 +31,7 @@
</svg>
</div>
<div class="flex items-center gap-2">
<%= link_to application.name, oauth_application_path(application),
class: "text-xl font-semibold text-white hover:text-primary transition-colors" %>
<%= link_to application.name, oauth_application_path(application), class: "text-xl font-semibold text-white hover:text-primary transition-colors" %>
<% if application.verified? %>
<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-green/20 text-green border border-green/30 rounded text-xs">
<svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">

View file

@ -25,8 +25,7 @@
<label class="block text-sm font-medium text-secondary mb-2"><%= t('.application_id') %></label>
<div class="flex items-center gap-2">
<code id="application_id" class="flex-1 px-3 py-2 bg-darkless border border-darkless rounded text-white font-mono text-sm break-all"><%= @application.uid %></code>
<button type="button" onclick="c(this, <%= @application.uid.to_json %>)"
class="px-3 py-2 bg-darkless hover:bg-secondary/20 text-white rounded transition-colors" title="Copy">
<button type="button" onclick="c(this, <%= @application.uid.to_json %>)" class="px-3 py-2 bg-darkless hover:bg-secondary/20 text-white rounded transition-colors" title="Copy">
<svg class="w-5 h-5 ci" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
@ -53,8 +52,7 @@
<% else %>
<div class="flex items-center gap-2">
<code id="secret" class="flex-1 px-3 py-2 bg-darkless border border-darkless rounded text-white font-mono text-sm break-all"><%= secret %></code>
<button type="button" onclick="c(this, <%= secret.to_json %>)"
class="px-3 py-2 bg-darkless hover:bg-secondary/20 text-white rounded transition-colors" title="Copy">
<button type="button" onclick="c(this, <%= secret.to_json %>)" class="px-3 py-2 bg-darkless hover:bg-secondary/20 text-white rounded transition-colors" title="Copy">
<svg class="w-5 h-5 ci" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
@ -204,11 +202,11 @@
<script>
function c(b, t) {
navigator.clipboard.writeText(t).then(() => {
b.querySelector('.ci').classList.add('hidden');
b.querySelector('.c').classList.remove('hidden');
b.querySelector(".ci").classList.add("hidden");
b.querySelector(".c").classList.remove("hidden");
setTimeout(() => {
b.querySelector('.ci').classList.remove('hidden');
b.querySelector('.c').classList.add('hidden');
b.querySelector(".ci").classList.remove("hidden");
b.querySelector(".c").classList.add("hidden");
}, 2000);
});
}

View file

@ -10,6 +10,6 @@
<script>
window.onload = function () {
document.forms['redirect_form'].submit();
document.forms["redirect_form"].submit();
};
</script>

View file

@ -29,7 +29,7 @@
<% @pre_auth.scopes.each do |scope| %>
<li class="flex items-center text-white">
<span class="inline-block w-2 h-2 bg-primary rounded-full mr-3"></span>
<%= t scope, scope: [:doorkeeper, :scopes] %>
<%= t scope, scope: %i[doorkeeper scopes] %>
</li>
<% end %>
</ul>
@ -62,7 +62,7 @@
<%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %>
<%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %>
<button type="submit" class="w-full px-4 py-3 bg-dark hover:bg-darkless border border-darkless text-gray-300 rounded transition-colors cursor-pointer flex items-center justify-center">
<span class="spinner hidden mr-2"><%= render "shared/spinner", class: "h-5 w-5" %></span>
<span class="spinner hidden mr-2"><%= render 'shared/spinner', class: 'h-5 w-5' %></span>
<span class="btn-text"><%= t('doorkeeper.authorizations.buttons.deny') %></span>
</button>
<% end %>

View file

@ -11,8 +11,7 @@
<label class="block text-sm font-medium text-secondary mb-2">Authorization Code</label>
<div class="flex items-center gap-2">
<code id="authorization_code" class="flex-1 px-3 py-2 bg-darkless border border-darkless rounded text-white font-mono text-sm break-all"><%= params[:code] %></code>
<button type="button" onclick="navigator.clipboard.writeText(<%= params[:code].to_json %>)"
class="px-3 py-2 bg-darkless hover:bg-secondary/20 text-white rounded transition-colors" title="Copy">
<button type="button" onclick="navigator.clipboard.writeText(<%= params[:code].to_json %>)" class="px-3 py-2 bg-darkless hover:bg-secondary/20 text-white rounded transition-colors" title="Copy">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>

View file

@ -1,7 +1,5 @@
<%= form_tag oauth_authorized_application_path(application), method: :delete do %>
<button type="submit"
onclick="return confirm(<%= t('doorkeeper.authorized_applications.confirmations.revoke').to_json %>)"
class="inline-flex items-center gap-2 px-4 py-2 bg-red hover:opacity-90 text-white font-medium rounded transition-colors duration-200">
<button type="submit" onclick="return confirm(<%= t('doorkeeper.authorized_applications.confirmations.revoke').to_json %>)" class="inline-flex items-center gap-2 px-4 py-2 bg-red hover:opacity-90 text-white font-medium rounded transition-colors duration-200">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>

View file

@ -21,9 +21,7 @@
</div>
<div>
<h3 class="text-xl font-semibold text-white"><%= application.name %></h3>
<p class="text-secondary text-sm">
Authorized <%= time_ago_in_words(application.created_at) %> ago
</p>
<p class="text-secondary text-sm">Authorized <%= time_ago_in_words(application.created_at) %> ago</p>
</div>
</div>

View file

@ -1,17 +1,8 @@
<h1>Verify your email address for Hackatime</h1>
<p>
Hi <%= h(@user.display_name) %>,
</p>
<p>
You've requested to add <%= @verification_request.email %> to your Hackatime account.
Click the link below to verify this email address:
</p>
<p>Hi <%= h(@user.display_name) %>,</p>
<p>You've requested to add <%= @verification_request.email %> to your Hackatime account. Click the link below to verify this email address:</p>
<p>
<%= link_to 'Verify email address', @verification_url %>
</p>
<p>
This link will expire in 30 minutes and can only be used once.
</p>
<p>
If you didn't request this email, you can safely ignore it.
</p>
<p>This link will expire in 30 minutes and can only be used once.</p>
<p>If you didn't request this email, you can safely ignore it.</p>

View file

@ -9,22 +9,18 @@
</div>
<div class="flex flex-col sm:flex-row gap-4">
<%= link_to "Go Home", root_path, class: "px-6 py-3 bg-primary text-white rounded-lg font-medium hover:bg-primary/75 transition-colors" %>
<button onclick="history.back()" class="px-6 py-3 bg-dark text-white rounded-lg font-medium hover:bg-darkless transition-colors border border-darkless cursor-pointer">
Go Back
</button>
<%= link_to 'Go Home', root_path, class: 'px-6 py-3 bg-primary text-white rounded-lg font-medium hover:bg-primary/75 transition-colors' %>
<button onclick="history.back()" class="px-6 py-3 bg-dark text-white rounded-lg font-medium hover:bg-darkless transition-colors border border-darkless cursor-pointer">Go Back</button>
</div>
<p class="mt-8 text-sm text-muted">
If this problem persists, please contact us on
<%= link_to "Slack", "https://hackclub.slack.com", class: "text-primary hover:underline", target: "_blank" %>.
<%= link_to 'Slack', 'https://hackclub.slack.com', class: 'text-primary hover:underline', target: '_blank' %>.
</p>
<% if @sentry_event_id.present? %>
<div class="mt-4 p-3 bg-darkless rounded-lg border border-dark">
<p class="text-xs text-muted">
Error ID: <code class="text-primary select-all"><%= @sentry_event_id %></code>
</p>
<p class="text-xs text-muted">Error ID: <code class="text-primary select-all"><%= @sentry_event_id %></code></p>
</div>
<% end %>
</div>

View file

@ -6,14 +6,14 @@
<div class="border rounded-lg p-4 bg-darkless">
<h2 class="font-bold text-2xl mb-2">Hackatime Desktop</h2>
<p>Desktop app for Hackatime. Runs on Mac, Windows, and Linux.</p>
<%= link_to "Source", "https://github.com/hackclub/hackatime-desktop", class: "text-primary hover:underline", allow_host: true %> |
<%= link_to "Install", "https://github.com/hackclub/hackatime-desktop/releases", class: "text-primary hover:underline", allow_host: true %>
<%= link_to 'Source', 'https://github.com/hackclub/hackatime-desktop', class: 'text-primary hover:underline', allow_host: true %> |
<%= link_to 'Install', 'https://github.com/hackclub/hackatime-desktop/releases', class: 'text-primary hover:underline', allow_host: true %>
</div>
<div class="border rounded-lg p-4 bg-darkless">
<h2 class="font-bold text-2xl mb-2">Cattatime</h2>
<p>A Tamagotchi system for Hackatime. Code, fill your cup, and get your pet rewards. Available for windows and mac.</p>
<%= link_to "Source", "https://github.com/joysudo/catatime/tree/master", class: "text-primary hover:underline", allow_host: true %> |
<%= link_to "Install", "https://github.com/joysudo/catatime/releases/", class: "text-primary hover:underline", allow_host: true %>
<%= link_to 'Source', 'https://github.com/joysudo/catatime/tree/master', class: 'text-primary hover:underline', allow_host: true %> |
<%= link_to 'Install', 'https://github.com/joysudo/catatime/releases/', class: 'text-primary hover:underline', allow_host: true %>
</div>
</div>
</div>

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html class="<%= Rails.env == "production" ? "production" : "development" %>" data-theme="dark">
<html class="<%= Rails.env == 'production' ? 'production' : 'development' %>" data-theme="dark">
<head>
<title><%= @page_title || content_for(:title) || "Hackatime" %></title>
<title><%= @page_title || content_for(:title) || 'Hackatime' %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
@ -41,124 +41,112 @@
<%= csp_meta_tag %>
<% if current_user %>
<meta name="user-is-superadmin" content="<%= current_user.admin_level == "superadmin" %>">
<meta name="user-is-admin" content="<%= current_user.admin_level == "admin" %>">
<meta name="user-is-viewer" content="<%= current_user.admin_level == "viewer" %>">
<meta name="user-is-superadmin" content="<%= current_user.admin_level == 'superadmin' %>">
<meta name="user-is-admin" content="<%= current_user.admin_level == 'admin' %>">
<meta name="user-is-viewer" content="<%= current_user.admin_level == 'viewer' %>">
<% end %>
<%= yield :head %>
<!-- JSON-LD Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Hackatime",
"alternateName": "Hack Club Hackatime",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "Any",
"description": "Track your coding time easily with Hackatime. A free tool to see how much time you spend programming in different languages and editors.",
"url": "https://hackatime.hackclub.com",
"downloadUrl": "https://hackatime.hackclub.com",
"sameAs": [
"https://github.com/hackclub/hackatime",
"https://hackatime.hackclub.com/docs"
],
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock"
},
"author": {
"@type": "Organization",
"name": "Hack Club",
"url": "https://hackclub.com"
},
"softwareVersion": "2.0",
"datePublished": "2025-01-01",
"license": "https://opensource.org/licenses/MIT",
"programmingLanguage": "Ruby",
"codeRepository": "https://github.com/hackclub/hackatime",
"supportingData": "Free coding time tracker",
"featureList": [
"Track coding time across 75+ editors",
"See which languages you use most",
"View daily coding statistics",
"Compare with other high schoolers",
"Free and open source"
]
}
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Hackatime",
"alternateName": "Hack Club Hackatime",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "Any",
"description": "Track your coding time easily with Hackatime. A free tool to see how much time you spend programming in different languages and editors.",
"url": "https://hackatime.hackclub.com",
"downloadUrl": "https://hackatime.hackclub.com",
"sameAs": ["https://github.com/hackclub/hackatime", "https://hackatime.hackclub.com/docs"],
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock"
},
"author": {
"@type": "Organization",
"name": "Hack Club",
"url": "https://hackclub.com"
},
"softwareVersion": "2.0",
"datePublished": "2025-01-01",
"license": "https://opensource.org/licenses/MIT",
"programmingLanguage": "Ruby",
"codeRepository": "https://github.com/hackclub/hackatime",
"supportingData": "Free coding time tracker",
"featureList": ["Track coding time across 75+ editors", "See which languages you use most", "View daily coding statistics", "Compare with other high schoolers", "Free and open source"]
}
</script>
<!-- Organization Schema -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Hack Club",
"url": "https://hackclub.com",
"logo": "https://hackclub.com/logo.png",
"sameAs": [
"https://twitter.com/hackclub",
"https://github.com/hackclub"
]
}
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Hack Club",
"url": "https://hackclub.com",
"logo": "https://hackclub.com/logo.png",
"sameAs": ["https://twitter.com/hackclub", "https://github.com/hackclub"]
}
</script>
<!-- WebSite Schema -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Hackatime",
"alternateName": "Hack Club Hackatime",
"url": "https://hackatime.hackclub.com"
}
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Hackatime",
"alternateName": "Hack Club Hackatime",
"url": "https://hackatime.hackclub.com"
}
</script>
<!-- FAQ Schema for Homepage -->
<% if request.path == "/" %>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
<script type="application/ld+json">
{
"@type": "Question",
"name": "What is Hackatime?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Hackatime is a free coding time tracker that helps you see how much time you spend programming. It tracks your coding time across different languages and editors."
}
},
{
"@type": "Question",
"name": "Is Hackatime free?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes! Hackatime is completely free to use. There are no paid plans or hidden costs."
}
},
{
"@type": "Question",
"name": "How is Hackatime different from WakaTime?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Hackatime is free and open source, while WakaTime has paid plans. Hackatime gives you all features for free and you can host it yourself."
}
},
{
"@type": "Question",
"name": "Is Hackatime the same as WakaTime?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No. Hackatime is a separate, independent open-source project built by Hack Club. While both track coding time, Hackatime is completely free and designed for high school students in the Hack Club community."
}
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is Hackatime?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Hackatime is a free coding time tracker that helps you see how much time you spend programming. It tracks your coding time across different languages and editors."
}
},
{
"@type": "Question",
"name": "Is Hackatime free?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes! Hackatime is completely free to use. There are no paid plans or hidden costs."
}
},
{
"@type": "Question",
"name": "How is Hackatime different from WakaTime?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Hackatime is free and open source, while WakaTime has paid plans. Hackatime gives you all features for free and you can host it yourself."
}
},
{
"@type": "Question",
"name": "Is Hackatime the same as WakaTime?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No. Hackatime is a separate, independent open-source project built by Hack Club. While both track coding time, Hackatime is completely free and designed for high school students in the Hack Club community."
}
}
]
}
]
}
</script>
</script>
<% end %>
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
@ -171,7 +159,7 @@
<%# Includes all stylesheet files in app/assets/stylesheets %>
<%= stylesheet_link_tag :app %>
<%= javascript_importmap_tags %>
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag 'tailwind', 'data-turbo-track': 'reload' %>
<% if Sentry.get_trace_propagation_meta %>
<%= sanitize Sentry.get_trace_propagation_meta, tags: %w[meta], attributes: %w[name content] %>
<% end %>
@ -179,17 +167,13 @@
<body class="<%= content_for(:body_class) %> flex min-h-screen bg-darker" data-controller="nav">
<% unless content_for?(:hide_nav) %>
<button class="mobile-nav-button"
data-action="click->nav#toggle"
data-nav-target="button"
aria-label="Toggle navigation menu"
aria-expanded="false">
<button class="mobile-nav-button" data-action="click->nav#toggle" data-nav-target="button" aria-label="Toggle navigation menu" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<div class="nav-overlay" data-nav-target="overlay" data-action="click->nav#close"></div>
<%= render "shared/nav" %>
<%= render 'shared/nav' %>
<% end %>
<!-- 250px is defined in nav.css -->
@ -198,29 +182,19 @@
<footer class="relative w-full mt-12 mb-5 p-2.5 text-center text-xs text-gray-600 hover:text-gray-300 transition-colors duration-200">
<div class="container">
<p>
Build <%= link_to Rails.application.config.git_version, Rails.application.config.commit_link, class: "text-inherit underline opacity-80 hover:opacity-100 transition-opacity duration-200" %>
from <%= time_ago_in_words(Rails.application.config.server_start_time) %> ago.
Build <%= link_to Rails.application.config.git_version, Rails.application.config.commit_link, class: 'text-inherit underline opacity-80 hover:opacity-100 transition-opacity duration-200' %> from <%= time_ago_in_words(Rails.application.config.server_start_time) %> ago.
<%= pluralize(Heartbeat.recent_count, 'heartbeat') %>
(<%= Heartbeat.recent_imported_count %> imported)
in the past 24 hours.
(DB: <%= pluralize(QueryCount::Counter.counter, "query") %>, <%= QueryCount::Counter.counter_cache %> cached)
(CACHE: <%= cache_stats[:hits] %> hits, <%= cache_stats[:misses] %> misses)
(<%= requests_per_second %>)
(<%= Heartbeat.recent_imported_count %> imported) in the past 24 hours. (DB: <%= pluralize(QueryCount::Counter.counter, 'query') %>, <%= QueryCount::Counter.counter_cache %> cached) (CACHE: <%= cache_stats[:hits] %> hits, <%= cache_stats[:misses] %> misses) (<%= requests_per_second %>)
</p>
<% if session[:impersonater_user_id] %>
<%= link_to "Stop impersonating", stop_impersonating_path, class: "text-primary font-bold hover:text-red-300 transition-colors duration-200", data: { turbo_prefetch: "false" } %>
<%= link_to 'Stop impersonating', stop_impersonating_path, class: 'text-primary font-bold hover:text-red-300 transition-colors duration-200', data: { turbo_prefetch: 'false' } %>
<% end %>
</div>
<%= render "static_pages/active_users_graph", hours: active_users_graph_data %>
<%= render 'static_pages/active_users_graph', hours: active_users_graph_data %>
</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 transform -translate-y-full transition-transform duration-300 ease-out hidden"
data-controller="currently-hacking"
data-currently-hacking-target="container"
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="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-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">
@ -229,28 +203,8 @@
</div>
</div>
</div>
<div data-currently-hacking-target="content" style="display: none;">
</div>
<div data-currently-hacking-target="content" style="display: none;"></div>
</div>
<%= render "shared/modal",
modal_id: "logout-modal",
title: "Woah hold on a sec",
description: "You sure you want to log out? You can sign back in later but that is a bit of a hassle...",
icon_svg: '<path fill="currentColor" d="M5 21q-.825 0-1.412-.587T3 19v-3q0-.425.288-.712T4 15t.713.288T5 16v3h14V5H5v3q0 .425-.288.713T4 9t-.712-.288T3 8V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm6.65-8H4q-.425 0-.712-.288T3 12t.288-.712T4 11h7.65L9.8 9.15q-.3-.3-.288-.7t.288-.7q.3-.3.713-.312t.712.287L14.8 11.3q.15.15.213.325t.062.375t-.062.375t-.213.325l-3.575 3.575q-.3.3-.712.288T9.8 16.25q-.275-.3-.288-.7t.288-.7z"/>',
icon_color: "text-primary",
buttons: [
{
text: "Go back",
class: "bg-dark hover:bg-darkless border border-darkless text-gray-300",
action: "click->modal#close"
},
{
text: "Log out now",
class: "bg-primary hover:bg-primary/75 text-white font-medium",
form: true,
url: signout_path,
method: "delete"
}
] %>
<%= render 'shared/modal', modal_id: 'logout-modal', title: 'Woah hold on a sec', description: 'You sure you want to log out? You can sign back in later but that is a bit of a hassle...', icon_svg: '<path fill="currentColor" d="M5 21q-.825 0-1.412-.587T3 19v-3q0-.425.288-.712T4 15t.713.288T5 16v3h14V5H5v3q0 .425-.288.713T4 9t-.712-.288T3 8V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm6.65-8H4q-.425 0-.712-.288T3 12t.288-.712T4 11h7.65L9.8 9.15q-.3-.3-.288-.7t.288-.7q.3-.3.713-.312t.712.287L14.8 11.3q.15.15.213.325t.062.375t-.062.375t-.213.325l-3.575 3.575q-.3.3-.712.288T9.8 16.25q-.275-.3-.288-.7t.288-.7z"/>', icon_color: 'text-primary', buttons: [{ text: 'Go back', class: 'bg-dark hover:bg-darkless border border-darkless text-gray-300', action: 'click->modal#close' }, { text: 'Log out now', class: 'bg-primary hover:bg-primary/75 text-white font-medium', form: true, url: signout_path, method: 'delete' }] %>
</body>
</html>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html class="<%= Rails.env == "production" ? "production" : "development" %>" data-theme="dark">
<html class="<%= Rails.env == 'production' ? 'production' : 'development' %>" data-theme="dark">
<head>
<title><%= t('doorkeeper.layouts.admin.title') %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
@ -11,21 +11,17 @@
<%= favicon_link_tag asset_path('favicon.png'), type: 'image/png' %>
<%= stylesheet_link_tag :app %>
<%= javascript_importmap_tags %>
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag 'tailwind', 'data-turbo-track': 'reload' %>
</head>
<body class="flex min-h-screen bg-darker" data-controller="nav">
<button class="mobile-nav-button"
data-action="click->nav#toggle"
data-nav-target="button"
aria-label="Toggle navigation menu"
aria-expanded="false">
<button class="mobile-nav-button" data-action="click->nav#toggle" data-nav-target="button" aria-label="Toggle navigation menu" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<div class="nav-overlay" data-nav-target="overlay" data-action="click->nav#close"></div>
<%= render "shared/nav" %>
<%= render 'shared/nav' %>
<main class="flex-1 lg:ml-[250px] lg:max-w-[calc(100%-250px)] p-5 mb-[100px] pt-16 lg:pt-5 transition-all duration-300 ease-in-out">
<%- if flash[:notice].present? %>

View file

@ -1,23 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title><%= t('doorkeeper.layouts.application.title') %></title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<title><%= t('doorkeeper.layouts.application.title') %></title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
<%= csrf_meta_tags %>
</head>
<body class="min-h-screen bg-darker text-white" style="margin: 0; padding: 0; display: flex; align-items: center; justify-content: center;">
<main class="w-full max-w-md px-5 py-8">
<%- if flash[:notice].present? %>
<div class="mb-6 p-4 bg-green-500/10 border border-green-500/20 rounded-lg text-green-400">
<%= flash[:notice] %>
</div>
<% end -%>
<%= stylesheet_link_tag 'tailwind', 'data-turbo-track': 'reload' %>
<%= csrf_meta_tags %>
</head>
<body class="min-h-screen bg-darker text-white" style="margin: 0; padding: 0; display: flex; align-items: center; justify-content: center;">
<main class="w-full max-w-md px-5 py-8">
<%- if flash[:notice].present? %>
<div class="mb-6 p-4 bg-green-500/10 border border-green-500/20 rounded-lg text-green-400">
<%= flash[:notice] %>
</div>
<% end -%>
<%= yield %>
</main>
</body>
<%= yield %>
</main>
</body>
</html>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html class="<%= Rails.env == "production" ? "production" : "development" %>" data-theme="dark">
<html class="<%= Rails.env == 'production' ? 'production' : 'development' %>" data-theme="dark">
<head>
<title><%= @title %> - Hackatime</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
@ -13,7 +13,7 @@
<%= favicon_link_tag asset_path('favicon.png'), type: 'image/png' %>
<%= stylesheet_link_tag :app %>
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag 'tailwind', 'data-turbo-track': 'reload' %>
</head>
<body class="bg-darker">

View file

@ -2,26 +2,32 @@
<div class="flex items-center p-2 hover:bg-dark transition-colors duration-200 <%= 'bg-dark border-l-4 border-l-primary' if e.user_id == current_user&.id %> <%= 'opacity-40 hover:opacity-60' if e.user.red? && current_user&.admin_level.in?(%w[admin superadmin]) %>">
<div class="w-12 shrink-0 text-center font-medium text-muted">
<% idx = offset + i %>
<%= case idx
when 0 then '<span class="text-2xl">🥇</span>'.html_safe
when 1 then '<span class="text-2xl">🥈</span>'.html_safe
when 2 then '<span class="text-2xl">🥉</span>'.html_safe
else "<span class=\"text-lg\">#{idx + 1}</span>".html_safe
end %>
<%=
case idx
when 0
'<span class="text-2xl">🥇</span>'.html_safe
when 1
'<span class="text-2xl">🥈</span>'.html_safe
when 2
'<span class="text-2xl">🥉</span>'.html_safe
else
"<span class=\"text-lg\">#{idx + 1}</span>".html_safe
end
%>
</div>
<div class="flex-1 mx-4 min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<%= render "shared/user_mention", user: e.user, show: [:profile_link, :verified_badge] %>
<%= render 'shared/user_mention', user: e.user, show: %i[profile_link verified_badge] %>
<% if (proj = active_projects&.dig(e.user_id)).present? %>
<span class="text-xs italic text-muted">
working on <%= link_to h(proj.project_name), proj.repo_url, target: "_blank", class: "text-accent hover:text-cyan-400 transition-colors" %>
working on <%= link_to h(proj.project_name), proj.repo_url, target: '_blank', class: 'text-accent hover:text-cyan-400 transition-colors' %>
<% dev_tool(nil, 'span') do %>
<%= link_to "🌌", visualize_git_url(proj.repo_url), target: "_blank", class: "ml-1" %>
<%= link_to '🌌', visualize_git_url(proj.repo_url), target: '_blank', class: 'ml-1' %>
<% end %>
</span>
<% end %>
<% if e.streak_count > 0 %>
<%= render "static_pages/streak", user: e.user, streak_count: e.streak_count, turbo_frame: false, icon_size: 16, show_super_class: true %>
<%= render 'static_pages/streak', user: e.user, streak_count: e.streak_count, turbo_frame: false, icon_size: 16, show_super_class: true %>
<% end %>
</div>
</div>

View file

@ -21,9 +21,7 @@
<% if mini_leaderboard_entries&.any? %>
<div class="bg-elevated rounded-xl border border-primary p-4 shadow-lg">
<p class="text-xs italic text-muted mb-4">
This leaderboard shows time logged in the last 24 hours (UTC time).
</p>
<p class="text-xs italic text-muted mb-4">This leaderboard shows time logged in the last 24 hours (UTC time).</p>
<div class="space-y-2">
<% mini_leaderboard_entries.each_with_index do |entry, idx| %>
<%
@ -32,36 +30,38 @@
%>
<div class="flex items-center p-3 rounded-lg bg-dark transition-colors duration-200 <%= 'bg-dark border border-primary' if entry.user_id == current_user&.id %>">
<% rank_emoji = case actual_rank
when 0 then "🥇"
when 1 then "🥈"
when 2 then "🥉"
else actual_rank + 1
end %>
<%
rank_emoji =
case actual_rank
when 0
'🥇'
when 1
'🥈'
when 2
'🥉'
else
actual_rank + 1
end
%>
<div class="w-8 text-center text-lg"><%= rank_emoji %></div>
<div class="flex-1 mx-3 min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<%= render "shared/user_mention", user: entry.user %>
<%= render 'shared/user_mention', user: entry.user %>
<% if entry.user == current_user && current_user.github_username.blank? %>
<span class="text-xs italic text-muted">
<%= link_to "Link active projects", my_settings_path(anchor: "user_github_account"), target: "_blank", class: "text-accent hover:text-cyan-400 transition-colors" %>
<%= link_to 'Link active projects', my_settings_path(anchor: 'user_github_account'), target: '_blank', class: 'text-accent hover:text-cyan-400 transition-colors' %>
</span>
<% end %>
<% if @active_projects&.dig(entry.user_id).present? %>
<span class="text-xs italic text-muted">
working on <%= link_to h(@active_projects[entry.user_id].project_name), @active_projects[entry.user_id].repo_url, target: "_blank", class: "text-accent hover:text-cyan-400 transition-colors" %>
working on <%= link_to h(@active_projects[entry.user_id].project_name), @active_projects[entry.user_id].repo_url, target: '_blank', class: 'text-accent hover:text-cyan-400 transition-colors' %>
<% dev_tool(nil, 'span') do %>
<%= link_to "🌌", visualize_git_url(@active_projects[entry.user_id].repo_url), target: "_blank", class: "ml-1" %>
<%= link_to '🌌', visualize_git_url(@active_projects[entry.user_id].repo_url), target: '_blank', class: 'ml-1' %>
<% end %>
</span>
<% end %>
<% if entry.streak_count > 0 %>
<%= render "static_pages/streak",
user: entry.user,
streak_count: entry.streak_count,
turbo_frame: false,
icon_size: 16,
show_super_class: true %>
<%= render 'static_pages/streak', user: entry.user, streak_count: entry.streak_count, turbo_frame: false, icon_size: 16, show_super_class: true %>
<% end %>
</div>
</div>

View file

@ -1,7 +1,5 @@
<div class="bg-elevated rounded-xl border border-primary p-4 shadow-lg">
<p class="text-xs italic text-muted mb-4 opacity-70">
This leaderboard shows time logged in the last 24 hours (UTC time).
</p>
<p class="text-xs italic text-muted mb-4 opacity-70">This leaderboard shows time logged in the last 24 hours (UTC time).</p>
<div class="space-y-2">
<% (current_user ? 5 : 3).times do %>
<div class="flex items-center p-3 rounded-lg bg-dark animate-pulse">

View file

@ -3,16 +3,12 @@
<h1 class="text-3xl font-bold text-white mb-4">Leaderboard</h1>
<div class="inline-flex rounded-full p-1 mb-4">
<%= link_to "Last 24 Hours", leaderboards_path(period_type: 'daily'),
class: "px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 #{@period_type == :daily ? 'bg-primary text-white' : 'text-muted bg-darkless hover:text-white'}" %>
<%= link_to "Last 7 Days", leaderboards_path(period_type: 'last_7_days'),
class: "px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 #{@period_type == :last_7_days ? 'bg-primary text-white' : 'text-muted bg-darkless hover:text-white'}" %>
<%= link_to 'Last 24 Hours', leaderboards_path(period_type: 'daily'), class: "px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 #{@period_type == :daily ? 'bg-primary text-white' : 'text-muted bg-darkless hover:text-white'}" %>
<%= link_to 'Last 7 Days', leaderboards_path(period_type: 'last_7_days'), class: "px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 #{@period_type == :last_7_days ? 'bg-primary text-white' : 'text-muted bg-darkless hover:text-white'}" %>
</div>
<% if current_user && current_user.github_uid.blank? %>
<div class="bg-darker border border-primary rounded-lg p-4 mb-6">
<%= link_to "Connect your GitHub", "/auth/github", class: "bg-primary hover:bg-primary/75 text-white font-medium mr-2 px-4 py-2 rounded-lg transition-colors duration-200" %> to qualify for the leaderboard.
</div>
<div class="bg-darker border border-primary rounded-lg p-4 mb-6"><%= link_to 'Connect your GitHub', '/auth/github', class: 'bg-primary hover:bg-primary/75 text-white font-medium mr-2 px-4 py-2 rounded-lg transition-colors duration-200' %> to qualify for the leaderboard.</div>
<% end %>
<div class="text-muted text-sm">
@ -20,18 +16,17 @@
<%= @leaderboard.date_range_text %>
<% if @leaderboard.finished_generating? && @leaderboard.persisted? %>
<span class="italic">
- Updated <%= time_ago_in_words(@leaderboard.updated_at) %> ago.
</span>
<span class="italic"> - Updated <%= time_ago_in_words(@leaderboard.updated_at) %> ago. </span>
<% end %>
<% else %>
<%= case @period_type
when :last_7_days
"#{(Date.current - 6.days).strftime('%B %d')} - #{Date.current.strftime('%B %d, %Y')}"
else
Date.current.strftime('%B %d, %Y')
end %>
<%=
case @period_type
when :last_7_days
"#{(Date.current - 6.days).strftime('%B %d')} - #{Date.current.strftime('%B %d, %Y')}"
else
Date.current.strftime('%B %d, %Y')
end
%>
<% end %>
</div>
</div>
@ -40,31 +35,29 @@
<% if @leaderboard&.persisted? %>
<% if @total_entries && @total_entries > 0 %>
<div id="leaderboard-entries" class="divide-y divide-gray-800" data-controller="infinite-scroll" data-infinite-scroll-url-value="<%= entries_leaderboards_path(period_type: @period_type) %>" data-infinite-scroll-page-value="1" data-infinite-scroll-total-value="<%= @total_entries %>">
<%= render "loading" %>
<%= render 'loading' %>
</div>
<% else %>
<div class="py-16 text-center">
<h3 class="text-xl font-medium text-white mb-2">No data available</h3>
<p class="text-muted">Check back later for <%= @period_type == :last_7_days ? "the last 7 days" : "the last 24 hours" %> results!</p>
<p class="text-muted">Check back later for <%= @period_type == :last_7_days ? 'the last 7 days' : 'the last 24 hours' %> results!</p>
</div>
<% end %>
<% unless @user_on_leaderboard && @untracked_entries != 0 %>
<div class="px-4 py-3 text-sm text-muted border-t border-primary">
Don't see yourself on the leaderboard? You're probably one of the
<%= pluralize(@untracked_entries, "user") %>
<%= pluralize(@untracked_entries, 'user') %>
who haven't
<%= link_to "updated their wakatime config", my_settings_path, target: "_blank", class: "text-accent hover:text-cyan-400 transition-colors" %>.
<%= link_to 'updated their wakatime config', my_settings_path, target: '_blank', class: 'text-accent hover:text-cyan-400 transition-colors' %>.
</div>
<% end %>
<% if @leaderboard.finished_generating? %>
<div class="px-4 py-2 text-xs italic text-muted border-t border-primary">
Generated in <%= @leaderboard.finished_generating_at - @leaderboard.created_at %> seconds
</div>
<div class="px-4 py-2 text-xs italic text-muted border-t border-primary">Generated in <%= @leaderboard.finished_generating_at - @leaderboard.created_at %> seconds</div>
<% end %>
<% else %>
<div class="py-16 text-center">
<h3 class="text-xl font-medium text-white mb-2">No data available</h3>
<p class="text-muted">Check back later for <%= @period_type == :last_7_days ? "the last 7 days" : "the last 24 hours" %> results!</p>
<p class="text-muted">Check back later for <%= @period_type == :last_7_days ? 'the last 7 days' : 'the last 24 hours' %> results!</p>
</div>
<% end %>
</div>

View file

@ -3,23 +3,4 @@
project_name = project[:project]
%>
<%= render "shared/modal",
modal_id: modal_id,
title: "Archive #{project_name}?",
description: "This project will be hidden from most stats and listings, however it will still be visible to you and any time logged will still count towards it. You can restore it anytime from the archived projects page.",
icon_svg: '<path fill="currentColor" d="m20.54 5.23l-1.39-1.68C18.88 3.21 18.47 3 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6.02 3 6.5V19c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6.5c0-.48-.17-.93-.46-1.27m-8.89 11.92L6.5 12H10v-2h4v2h3.5l-5.15 5.15c-.19.19-.51.19-.7 0M5.12 5l.81-1h12l.94 1z"/>',
max_width: "max-w-md",
buttons: [
{
text: "Cancel",
class: "bg-dark hover:bg-darkless border border-darkless text-gray-300",
action: "click->modal#close"
},
{
text: "Archive",
class: "bg-primary hover:bg-primary/75 text-white font-medium",
form: true,
url: archive_my_project_repo_mapping_path(CGI.escape(project_name)),
method: "patch"
}
] %>
<%= render 'shared/modal', modal_id: modal_id, title: "Archive #{project_name}?", description: 'This project will be hidden from most stats and listings, however it will still be visible to you and any time logged will still count towards it. You can restore it anytime from the archived projects page.', icon_svg: '<path fill="currentColor" d="m20.54 5.23l-1.39-1.68C18.88 3.21 18.47 3 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6.02 3 6.5V19c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6.5c0-.48-.17-.93-.46-1.27m-8.89 11.92L6.5 12H10v-2h4v2h3.5l-5.15 5.15c-.19.19-.51.19-.7 0M5.12 5l.81-1h12l.94 1z"/>', max_width: 'max-w-md', buttons: [{ text: 'Cancel', class: 'bg-dark hover:bg-darkless border border-darkless text-gray-300', action: 'click->modal#close' }, { text: 'Archive', class: 'bg-primary hover:bg-primary/75 text-white font-medium', form: true, url: archive_my_project_repo_mapping_path(CGI.escape(project_name)), method: 'patch' }] %>

View file

@ -12,40 +12,16 @@
id: form_id,
class: "w-full space-y-4" do |f| %>
<div class="space-y-2">
<%= f.label :project_name, "Project Name", class: "block text-sm font-semibold text-white" %>
<%= f.text_field :project_name,
value: project_name,
disabled: true,
class: "w-full px-4 py-3 bg-darkless text-secondary border border-darkless rounded-lg cursor-not-allowed" %>
<%= f.label :project_name, 'Project Name', class: 'block text-sm font-semibold text-white' %>
<%= f.text_field :project_name, value: project_name, disabled: true, class: 'w-full px-4 py-3 bg-darkless text-secondary border border-darkless rounded-lg cursor-not-allowed' %>
<p class="text-xs text-gray-400">Project name cannot be changed.</p>
</div>
<div class="space-y-2 mb-4">
<%= f.label :repo_url, "Repository URL", class: "block text-sm font-semibold text-white" %>
<%= f.url_field :repo_url,
value: repo_url,
placeholder: "https://github.com/username/repo",
class: "w-full px-4 py-3 bg-darkless text-white border border-darkless rounded-lg focus:border-primary focus:outline-none transition-colors" %>
<%= f.label :repo_url, 'Repository URL', class: 'block text-sm font-semibold text-white' %>
<%= f.url_field :repo_url, value: repo_url, placeholder: 'https://github.com/username/repo', class: 'w-full px-4 py-3 bg-darkless text-white border border-darkless rounded-lg focus:border-primary focus:outline-none transition-colors' %>
</div>
<% end %>
<% end %>
<%= render "shared/modal",
modal_id: modal_id,
title: "Edit Project Mapping",
description: "We try to autodetect your Git repository, but you can manually specify it if needed.",
max_width: "max-w-lg",
custom: form_content,
buttons: [
{
text: "Cancel",
class: "bg-dark hover:bg-darkless border border-darkless text-gray-300",
action: "click->modal#close"
},
{
text: "Update Project",
class: "bg-primary hover:bg-primary/75 text-white font-medium",
form: true,
form_id: form_id
}
] %>
<%= render 'shared/modal', modal_id: modal_id, title: 'Edit Project Mapping', description: 'We try to autodetect your Git repository, but you can manually specify it if needed.', max_width: 'max-w-lg', custom: form_content, buttons: [{ text: 'Cancel', class: 'bg-dark hover:bg-darkless border border-darkless text-gray-300', action: 'click->modal#close' }, { text: 'Update Project', class: 'bg-primary hover:bg-primary/75 text-white font-medium', form: true, form_id: form_id }] %>

View file

@ -1,24 +1,3 @@
<%
modal_id = "unarchive-project-modal-#{project[:project]&.parameterize || 'unknown'}"
%>
<% modal_id = "unarchive-project-modal-#{project[:project]&.parameterize || 'unknown'}" %>
<%= render "shared/modal",
modal_id: modal_id,
title: "Restore #{project[:project]}?",
description: "This will restore the project to your main projects list and it will appear in stats again. This will not affect any time logged on this project.",
icon_svg: '<path fill="currentColor" d="m20.55 5.22l-1.39-1.68A1.51 1.51 0 0 0 18 3H6c-.47 0-.88.21-1.15.55L3.46 5.22C3.17 5.57 3 6.01 3 6.5V19a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V6.5c0-.49-.17-.93-.45-1.28m-8.2 4.63L17.5 15H14v2h-4v-2H6.5l5.15-5.15c.19-.19.51-.19.7 0M5.12 5l.82-1h12l.93 1z"/>',
max_width: "max-w-md",
buttons: [
{
text: "Cancel",
class: "bg-dark hover:bg-darkless border border-darkless text-gray-300",
action: "click->modal#close"
},
{
text: "Restore",
class: "bg-primary hover:bg-primary/75 text-white font-medium",
form: true,
url: unarchive_my_project_repo_mapping_path(CGI.escape(project[:project_key] || project[:project])),
method: "patch"
}
] %>
<%= render 'shared/modal', modal_id: modal_id, title: "Restore #{project[:project]}?", description: 'This will restore the project to your main projects list and it will appear in stats again. This will not affect any time logged on this project.', icon_svg: '<path fill="currentColor" d="m20.55 5.22l-1.39-1.68A1.51 1.51 0 0 0 18 3H6c-.47 0-.88.21-1.15.55L3.46 5.22C3.17 5.57 3 6.01 3 6.5V19a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V6.5c0-.49-.17-.93-.45-1.28m-8.2 4.63L17.5 15H14v2h-4v-2H6.5l5.15-5.15c.19-.19.51-.19.7 0M5.12 5l.82-1h12l.93 1z"/>', max_width: 'max-w-md', buttons: [{ text: 'Cancel', class: 'bg-dark hover:bg-darkless border border-darkless text-gray-300', action: 'click->modal#close' }, { text: 'Restore', class: 'bg-primary hover:bg-primary/75 text-white font-medium', form: true, url: unarchive_my_project_repo_mapping_path(CGI.escape(project[:project_key] || project[:project])), method: 'patch' }] %>

View file

@ -1,4 +1,4 @@
<% content_for(:title) { "Edit Project Mapping" } %>
<% content_for(:title) { 'Edit Project Mapping' } %>
<div class="max-w-lg mx-auto mt-8">
<div class="bg-elevated rounded-xl p-6 border border-darkless">
@ -17,25 +17,19 @@
local: true,
class: "w-full space-y-4" do |f| %>
<div class="space-y-2">
<%= f.label :project_name, "Project Name", class: "block text-sm font-semibold text-white" %>
<%= f.text_field :project_name,
value: @project_repo_mapping.project_name,
disabled: true,
class: "w-full px-4 py-3 bg-darkless text-secondary border border-darkless rounded-lg cursor-not-allowed" %>
<%= f.label :project_name, 'Project Name', class: 'block text-sm font-semibold text-white' %>
<%= f.text_field :project_name, value: @project_repo_mapping.project_name, disabled: true, class: 'w-full px-4 py-3 bg-darkless text-secondary border border-darkless rounded-lg cursor-not-allowed' %>
<p class="text-xs text-gray-400">Project name cannot be changed.</p>
</div>
<div class="space-y-2 mb-4">
<%= f.label :repo_url, "Repository URL", class: "block text-sm font-semibold text-white" %>
<%= f.url_field :repo_url,
value: @project_repo_mapping.repo_url,
placeholder: "https://github.com/username/repo",
class: "w-full px-4 py-3 bg-darkless text-white border border-darkless rounded-lg focus:border-primary focus:outline-none transition-colors" %>
<%= f.label :repo_url, 'Repository URL', class: 'block text-sm font-semibold text-white' %>
<%= f.url_field :repo_url, value: @project_repo_mapping.repo_url, placeholder: 'https://github.com/username/repo', class: 'w-full px-4 py-3 bg-darkless text-white border border-darkless rounded-lg focus:border-primary focus:outline-none transition-colors' %>
</div>
<div class="flex gap-3 justify-end">
<%= link_to "Cancel", my_projects_path, class: "px-4 py-2 border border-gray-600 text-gray-300 hover:bg-darkless rounded-lg transition-colors" %>
<%= f.submit "Update Project", class: "px-4 py-2 bg-primary text-white hover:bg-red rounded-lg font-medium cursor-pointer transition-colors" %>
<%= link_to 'Cancel', my_projects_path, class: 'px-4 py-2 border border-gray-600 text-gray-300 hover:bg-darkless rounded-lg transition-colors' %>
<%= f.submit 'Update Project', class: 'px-4 py-2 bg-primary text-white hover:bg-red rounded-lg font-medium cursor-pointer transition-colors' %>
</div>
<% end %>
</div>

View file

@ -4,10 +4,8 @@
<% archived_count = current_user.project_repo_mappings.archived.count %>
<% if archived_count > 0 %>
<div class="project-toggle-group">
<%= link_to "Active", my_projects_path(show_archived: false),
class: "project-toggle-btn #{params[:show_archived] != 'true' ? 'active' : 'inactive'}" %>
<%= link_to "Archived", my_projects_path(show_archived: true),
class: "project-toggle-btn #{params[:show_archived] == 'true' ? 'active' : 'inactive'}" %>
<%= link_to 'Active', my_projects_path(show_archived: false), class: "project-toggle-btn #{params[:show_archived] != 'true' ? 'active' : 'inactive'}" %>
<%= link_to 'Archived', my_projects_path(show_archived: true), class: "project-toggle-btn #{params[:show_archived] == 'true' ? 'active' : 'inactive'}" %>
</div>
<% end %>
</div>
@ -16,12 +14,12 @@
<% if current_user.github_uid.blank? %>
<div class="text-red-400 mb-4">
Heads up! You can't link your projects to GitHub until you connect your GitHub account.
<%= link_to "Sign in with GitHub", github_auth_path, class: "btn btn-primary text-white underline" %>
<%= link_to 'Sign in with GitHub', github_auth_path, class: 'btn btn-primary text-white underline' %>
</div>
<% end %>
<%= render "shared/interval_selector" %>
<%= render 'shared/interval_selector' %>
<%= turbo_frame_tag "project_durations", src: project_durations_static_pages_path(interval: params[:interval], from: params[:from], to: params[:to], show_archived: params[:show_archived]), target: "_top" do %>
<%= render "static_pages/project_durations_skeleton", count: @project_count %>
<%= render 'static_pages/project_durations_skeleton', count: @project_count %>
<% end %>

View file

@ -2,7 +2,7 @@
<% if daily_durations.present? %>
<div>
<h2 class="text-xl font-bold mb-4">Activity</h2>
<%= render "static_pages/activity_graph", daily_durations: daily_durations, length_of_busiest_day: 8.hours.to_i, user_tz: user_tz %>
<%= render 'static_pages/activity_graph', daily_durations: daily_durations, length_of_busiest_day: 8.hours.to_i, user_tz: user_tz %>
</div>
<% end %>
<% end %>

View file

@ -6,7 +6,7 @@
<% max_duration = languages.values.map(&:to_f).select { |d| d.finite? && d > 0 }.max || 1 %>
<% languages.each do |language, duration| %>
<div class="flex items-center gap-3">
<div class="w-24 text-sm text-gray-300 truncate"><%= ApplicationController.helpers.display_language_name(language) || "Unknown" %></div>
<div class="w-24 text-sm text-gray-300 truncate"><%= ApplicationController.helpers.display_language_name(language) || 'Unknown' %></div>
<div class="flex-1 h-6 bg-darkless rounded-full overflow-hidden relative">
<div class="h-full bg-primary rounded-full flex items-center justify-end pr-2" style="width: <%= [(duration.to_f.finite? ? duration.to_f : 0) / max_duration * 100, 100].min.round %>%">
<span class="text-xs font-medium text-white"><%= ApplicationController.helpers.short_time_simple(duration) %></span>

View file

@ -1,13 +1,11 @@
<% if projects.present? && projects.size > 0 %>
<%
max = projects.map { |p| p[:duration].to_f }.select { |d| d.finite? && d > 0 }.max || 1
%>
<% max = projects.map { |p| p[:duration].to_f }.select { |d| d.finite? && d > 0 }.max || 1 %>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<% projects.each do |project| %>
<div class="border border-primary rounded-xl p-5 transition-all duration-300 flex flex-col gap-3">
<div class="flex justify-between items-start gap-2">
<h3 class="text-lg font-semibold text-white truncate flex-1" title="<%= h(project[:project]) %>">
<%= h(project[:project].presence || "Unknown") %>
<%= h(project[:project].presence || 'Unknown') %>
</h3>
<% if project[:repo_url].present? %>
<%= link_to project[:repo_url], target: "_blank", title: "View repository", class: "p-2 rounded-lg bg-white/5 hover:bg-white/10 transition-colors duration-200 shrink-0" do %>
@ -23,9 +21,7 @@
</div>
<div class="w-full h-2 bg-darkless rounded-full overflow-hidden">
<div
class="h-full bg-primary rounded-full"
style="width: <%= [(project[:duration].to_f.finite? ? project[:duration].to_f : 0) / max.to_f * 100, 100].min %>%;"></div>
<div class="h-full bg-primary rounded-full" style="width: <%= [(project[:duration].to_f.finite? ? project[:duration].to_f : 0) / max.to_f * 100, 100].min %>%;"></div>
</div>
</div>
<% end %>

View file

@ -2,7 +2,7 @@
<% if projects.present? && projects.size > 0 %>
<div class="mb-6">
<h2 class="text-xl font-bold mb-4">Top Projects <span class="text-gray-400 font-normal text-base">(Past Month)</span></h2>
<%= render "profiles/project_cards", projects: projects %>
<%= render 'profiles/project_cards', projects: projects %>
</div>
<% end %>
<% end %>

View file

@ -2,6 +2,6 @@
<div class="text-center py-16">
<h1 class="text-3xl font-bold mb-2">User not found</h1>
<p class="text-gray-400 mb-6">We couldn't find a user with that username... Usernames are case-sensitive if that helps.</p>
<%= link_to "Go back home", root_path, class: "inline-block px-6 py-3 bg-primary text-white rounded font-bold hover:bg-primary/75 transition-colors" %>
<%= link_to 'Go back home', root_path, class: 'inline-block px-6 py-3 bg-primary text-white rounded font-bold hover:bg-primary/75 transition-colors' %>
</div>
</div>

View file

@ -8,7 +8,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" viewBox="0 0 256 256" fill="#EC3750" title="Verified"><path d="M225.86 102.82c-3.77-3.94-7.67-8-9.14-11.57c-1.36-3.27-1.44-8.69-1.52-13.94c-.15-9.76-.31-20.82-8-28.51s-18.75-7.85-28.51-8c-5.25-.08-10.67-.16-13.94-1.52c-3.56-1.47-7.63-5.37-11.57-9.14C146.28 23.51 138.44 16 128 16s-18.27 7.51-25.18 14.14c-3.94 3.77-8 7.67-11.57 9.14c-3.25 1.36-8.69 1.44-13.94 1.52c-9.76.15-20.82.31-28.51 8s-7.8 18.75-8 28.51c-.08 5.25-.16 10.67-1.52 13.94c-1.47 3.56-5.37 7.63-9.14 11.57C23.51 109.72 16 117.56 16 128s7.51 18.27 14.14 25.18c3.77 3.94 7.67 8 9.14 11.57c1.36 3.27 1.44 8.69 1.52 13.94c.15 9.76.31 20.82 8 28.51s18.75 7.85 28.51 8c5.25.08 10.67.16 13.94 1.52c3.56 1.47 7.63 5.37 11.57 9.14c6.9 6.63 14.74 14.14 25.18 14.14s18.27-7.51 25.18-14.14c3.94-3.77 8-7.67 11.57-9.14c3.27-1.36 8.69-1.44 13.94-1.52c9.76-.15 20.82-.31 28.51-8s7.85-18.75 8-28.51c.08-5.25.16-10.67 1.52-13.94c1.47-3.56 5.37-7.63 9.14-11.57c6.63-6.9 14.14-14.74 14.14-25.18s-7.51-18.27-14.14-25.18m-52.2 6.84l-56 56a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 11.32" /></svg>
<% end %>
<% if @profile_visible && @streak_days && @streak_days > 0 %>
<%= render "static_pages/streak", user: @user, streak_count: @streak_days, turbo_frame: false, icon_size: 20, show_text: true %>
<%= render 'static_pages/streak', user: @user, streak_count: @streak_days, turbo_frame: false, icon_size: 20, show_text: true %>
<% end %>
</div>
<% if @user.username.present? %>
@ -25,25 +25,25 @@
<% if @profile_visible %>
<%= turbo_frame_tag "profile_time_stats", src: profile_time_stats_path(@user.username), loading: :lazy do %>
<%= render "profiles/time_stats_skeleton" %>
<%= render 'profiles/time_stats_skeleton' %>
<% end %>
<%= turbo_frame_tag "profile_projects", src: profile_projects_path(@user.username), loading: :lazy do %>
<%= render "profiles/projects_skeleton" %>
<%= render 'profiles/projects_skeleton' %>
<% end %>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<%= turbo_frame_tag "profile_languages", src: profile_languages_path(@user.username), loading: :lazy do %>
<%= render "profiles/languages_skeleton" %>
<%= render 'profiles/languages_skeleton' %>
<% end %>
<%= turbo_frame_tag "profile_editors", src: profile_editors_path(@user.username), loading: :lazy do %>
<%= render "profiles/editors_skeleton" %>
<%= render 'profiles/editors_skeleton' %>
<% end %>
</div>
<%= turbo_frame_tag "profile_activity", src: profile_activity_path(@user.username), loading: :lazy do %>
<%= render "profiles/activity_skeleton" %>
<%= render 'profiles/activity_skeleton' %>
<% end %>
<% else %>
<div class="text-center py-16">

View file

@ -1,2 +1,2 @@
<%= render "shared/user_mention", user: activity.owner %>
<%= render 'shared/user_mention', user: activity.owner %>
just hit their first 7 day coding streak!

View file

@ -1,2 +1,2 @@
<%= render "shared/user_mention", user: activity.owner %>
<%= render 'shared/user_mention', user: activity.owner %>
was just sent a letter for '<%= activity.parameters[:humanized_mission_type] %>'

View file

@ -1,3 +1,2 @@
<%= render "shared/user_mention", user: activity.owner %>
just finished coding on <%= activity.parameters['project'] %>
for <%= short_time_simple(activity.parameters['duration_seconds']) %>
<%= render 'shared/user_mention', user: activity.owner %>
just finished coding on <%= activity.parameters['project'] %> for <%= short_time_simple(activity.parameters['duration_seconds']) %>

View file

@ -1,2 +1,2 @@
<%= render "shared/user_mention", user: activity.owner %>
<%= render 'shared/user_mention', user: activity.owner %>
just started tracking their coding time on <%= activity.parameters['project'] %>!

View file

@ -1,2 +1,2 @@
<%= render "shared/user_mention", user: activity.owner %>
<%= render 'shared/user_mention', user: activity.owner %>
just signed in for the first time

View file

@ -1,2 +1,2 @@
<%= render "shared/user_mention", user: activity.owner %>
<%= render 'shared/user_mention', user: activity.owner %>
just started working on <%= activity.parameters['project'] %>

View file

@ -1,6 +1,6 @@
<% content_for :title, "Successfully signed in!" %>
<% content_for :title, 'Successfully signed in!' %>
<script>
setTimeout(function() {
setTimeout(function () {
window.close();
}, 1000);
</script>

View file

@ -40,9 +40,9 @@
const url = new URL(window.location.href);
// Clear existing parameters
url.searchParams.delete('interval');
url.searchParams.delete('from');
url.searchParams.delete('to');
url.searchParams.delete("interval");
url.searchParams.delete("from");
url.searchParams.delete("to");
// Add new parameters
Object.entries(params).forEach(([key, value]) => {
@ -53,21 +53,21 @@
}
function updateProjectsFrame(url) {
let baseUrl = <%== project_durations_static_pages_path.to_json %>;
let baseUrl = <%= = project_durations_static_pages_path.to_json %>;
const params = new URLSearchParams(new URL(url).search);
const frameUrl = baseUrl + '?' + params.toString();
const frameUrl = baseUrl + "?" + params.toString();
const frame = document.getElementById('project_durations');
const frame = document.getElementById("project_durations");
if (frame) {
console.log('Updating frame with URL:', frameUrl);
console.log("Updating frame with URL:", frameUrl);
frame.src = frameUrl;
try {
frame.reload();
console.log('Frame reload called successfully');
console.log("Frame reload called successfully");
} catch (e) {
console.error('Error reloading frame:', e);
console.error("Error reloading frame:", e);
}
history.replaceState({}, '', url);
history.replaceState({}, "", url);
} else {
console.error('Frame with ID "project_durations" not found');
window.location.href = url;
@ -75,7 +75,7 @@
}
function updateDropdownLabel(label) {
const labelElement = document.getElementById('interval-dropdown-label');
const labelElement = document.getElementById("interval-dropdown-label");
if (labelElement) {
labelElement.textContent = label;
}
@ -83,16 +83,16 @@
function selectInterval(interval, label) {
updateDropdownLabel(label);
document.getElementById('interval-dropdown-menu').style.display = 'none';
document.getElementById("interval-dropdown-menu").style.display = "none";
updateWithParams({ interval });
}
function applyCustomRange() {
const start = document.getElementById('custom-start').value;
const end = document.getElementById('custom-end').value;
const start = document.getElementById("custom-start").value;
const end = document.getElementById("custom-end").value;
if (start || end) {
let label = 'Custom Range';
let label = "Custom Range";
if (start && end) {
label = `${start} to ${end}`;
} else if (start) {
@ -102,30 +102,30 @@
}
updateDropdownLabel(label);
document.getElementById('interval-dropdown-menu').style.display = 'none';
updateWithParams({ interval: 'custom', from: start, to: end });
document.getElementById("interval-dropdown-menu").style.display = "none";
updateWithParams({ interval: "custom", from: start, to: end });
}
}
function toggleIntervalDropdown() {
const menu = document.getElementById('interval-dropdown-menu');
const arrow = document.getElementById('interval-dropdown-arrow');
const isOpen = menu.style.display === 'block';
const menu = document.getElementById("interval-dropdown-menu");
const arrow = document.getElementById("interval-dropdown-arrow");
const isOpen = menu.style.display === "block";
menu.style.display = isOpen ? 'none' : 'block';
arrow.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(180deg)';
document.addEventListener('mousedown', closeDropdownOnClickOutside);
menu.style.display = isOpen ? "none" : "block";
arrow.style.transform = isOpen ? "rotate(0deg)" : "rotate(180deg)";
document.addEventListener("mousedown", closeDropdownOnClickOutside);
}
function closeDropdownOnClickOutside(e) {
const menu = document.getElementById('interval-dropdown-menu');
const trigger = document.getElementById('interval-dropdown-trigger');
const arrow = document.getElementById('interval-dropdown-arrow');
const menu = document.getElementById("interval-dropdown-menu");
const trigger = document.getElementById("interval-dropdown-trigger");
const arrow = document.getElementById("interval-dropdown-arrow");
if (!menu.contains(e.target) && !trigger.contains(e.target)) {
menu.style.display = 'none';
arrow.style.transform = 'rotate(0deg)';
document.removeEventListener('mousedown', closeDropdownOnClickOutside);
menu.style.display = "none";
arrow.style.transform = "rotate(0deg)";
document.removeEventListener("mousedown", closeDropdownOnClickOutside);
}
}
</script>

View file

@ -1,20 +1,16 @@
<%
modal_id ||= "modal-#{SecureRandom.hex(4)}"
icon_svg ||= nil
icon_color ||= "text-primary"
title ||= "Confirm"
icon_color ||= 'text-primary'
title ||= 'Confirm'
description ||= nil
buttons ||= []
max_width ||= "max-w-md"
max_width ||= 'max-w-md'
custom ||= nil
%>
<div id="<%= modal_id %>"
class="fixed inset-0 flex items-center justify-center z-9999 opacity-0 pointer-events-none transition-opacity duration-300 ease-in-out hidden"
style="background-color: rgba(0, 0, 0, 0.5);backdrop-filter: blur(4px);"
data-controller="modal">
<div class="bg-dark border border-primary rounded-lg p-6 <%= max_width %> w-full mx-4 flex flex-col items-center justify-center transform scale-95 transition-transform duration-300 ease-in-out"
data-modal-target="content">
<div id="<%= modal_id %>" class="fixed inset-0 flex items-center justify-center z-9999 opacity-0 pointer-events-none transition-opacity duration-300 ease-in-out hidden" style="background-color: rgba(0, 0, 0, 0.5);backdrop-filter: blur(4px);" data-controller="modal">
<div class="bg-dark border border-primary rounded-lg p-6 <%= max_width %> w-full mx-4 flex flex-col items-center justify-center transform scale-95 transition-transform duration-300 ease-in-out" data-modal-target="content">
<div class="flex flex-col items-center w-full">
<% if icon_svg %>
<div class="mb-4 flex justify-center w-full">
@ -42,9 +38,7 @@
<div class="flex-1 min-w-0">
<% if button[:form] %>
<% if button[:form_id] %>
<button type="submit"
form="<%= button[:form_id] %>"
class="w-full h-10 px-4 rounded-lg transition-colors duration-200 font-medium cursor-pointer m-0 <%= button[:class] %>">
<button type="submit" form="<%= button[:form_id] %>" class="w-full h-10 px-4 rounded-lg transition-colors duration-200 font-medium cursor-pointer m-0 <%= button[:class] %>">
<%= button[:text] %>
</button>
<% else %>
@ -53,16 +47,13 @@
<% if button[:method] && button[:method] != 'post' %>
<input type="hidden" name="_method" value="<%= button[:method] %>">
<% end %>
<button type="submit"
class="w-full h-10 px-4 rounded-lg transition-colors duration-200 font-medium cursor-pointer m-0 <%= button[:class] %>">
<button type="submit" class="w-full h-10 px-4 rounded-lg transition-colors duration-200 font-medium cursor-pointer m-0 <%= button[:class] %>">
<%= button[:text] %>
</button>
</form>
<% end %>
<% else %>
<button type="button"
data-action="<%= button[:action] || 'click->modal#close' %>"
class="w-full h-10 px-4 rounded-lg transition-colors duration-200 cursor-pointer m-0 <%= button[:class] %>">
<button type="button" data-action="<%= button[:action] || 'click->modal#close' %>" class="w-full h-10 px-4 rounded-lg transition-colors duration-200 cursor-pointer m-0 <%= button[:class] %>">
<%= button[:text] %>
</button>
<% end %>

View file

@ -2,9 +2,7 @@
<label class="filter-label">▼ <%= label %></label>
<div class="custom-select" id="<%= param %>-select" data-param="<%= param %>">
<div class="select-header-container">
<div class="select-header">
Filter by <%= label.downcase %>...
</div>
<div class="select-header">Filter by <%= label.downcase %>...</div>
<button class="clear-button">×</button>
</div>
<div class="options-container">
@ -12,10 +10,7 @@
<div class="options-list">
<% values.reject(&:blank?).each do |value| %>
<label class="option">
<input
type="checkbox"
value="<%= value %>"
<%= 'checked' if selected&.include?(value) %>>
<input type="checkbox" value="<%= value %>" <%= 'checked' if selected&.include?(value) %>>
<span><%= value %></span>
</label>
<% end %>

View file

@ -1,27 +1,30 @@
<aside class="flex flex-col min-h-screen w-[250px] bg-dark text-white px-2 py-4 rounded-r-lg overflow-y-auto lg:block" data-nav-target="nav" style="scrollbar-width: none; -ms-overflow-style: none;">
<div class="space-y-4">
<% flash.each do |name, msg| %>
<% c = case name.to_sym
when :notice
"border-green text-green"
else
"border-primary text-primary"
end %>
<%
c =
case name.to_sym
when :notice
'border-green text-green'
else
'border-primary text-primary'
end
%>
<div>
<div class="rounded-lg border text-center text-lg px-3 py-2 mb-2 <%= c %>"><%= msg %></div>
<div class="rounded-lg border text-center text-lg px-3 py-2 mb-2 <%= c %>"><%= msg %></div>
</div>
<% end %>
<% if current_user %>
<div class="px-2 rounded-lg flex flex-col items-center gap-2">
<%= render "shared/user_mention", user: current_user %>
<%= render "static_pages/streak", user: current_user, show_text: true, turbo_frame: false %>
<%= render 'shared/user_mention', user: current_user %>
<%= render 'static_pages/streak', user: current_user, show_text: true, turbo_frame: false %>
<% if current_user.admin_level != 0 %>
<%= render "static_pages/admin_level", user: current_user %>
<%= render 'static_pages/admin_level', user: current_user %>
<% end %>
</div>
<% else %>
<div class="mb-1">
<%= link_to "Login", slack_auth_path, class: "block px-2 py-1 rounded-lg transition text-white font-bold bg-primary hover:bg-secondary text-lg" %>
<%= link_to 'Login', slack_auth_path, class: 'block px-2 py-1 rounded-lg transition text-white font-bold bg-primary hover:bg-secondary text-lg' %>
</div>
<% end %>
<div>
@ -80,12 +83,7 @@
<% end %>
</div>
<div>
<button
type="button"
onclick="showLogout()"
class="w-full text-left cursor-pointer block px-[15px] py-2.5 rounded-lg transition hover:text-primary hover:bg-darkless"
>Logout
</button>
<button type="button" onclick="showLogout()" class="w-full text-left cursor-pointer block px-[15px] py-2.5 rounded-lg transition hover:text-primary hover:bg-darkless">Logout</button>
</div>
<% end %>
<% dev_tool(nil, "div") do %>

View file

@ -11,7 +11,6 @@
<div class="h-8 w-24 bg-darkless rounded mt-2"></div>
</div>
</div>
<% when :stat_cards %>
<div class="stats-section">
<% 6.times do %>
@ -25,7 +24,6 @@
</div>
<% end %>
</div>
<% when :bar_graph %>
<div class="card animate-pulse">
<div class="h-6 w-40 bg-darkless rounded mb-4"></div>
@ -40,7 +38,6 @@
<% end %>
</div>
</div>
<% when :pie_chart %>
<div class="card animate-pulse">
<div class="h-6 w-32 bg-darkless rounded mb-4"></div>
@ -56,7 +53,6 @@
</div>
</div>
</div>
<% when :activity_graph %>
<div class="w-full overflow-x-auto mt-6 pb-2.5 animate-pulse">
<div class="grid grid-rows-7 grid-flow-col gap-1 w-full lg:w-1/2">
@ -66,12 +62,10 @@
</div>
<div class="h-3 w-48 bg-darkless rounded mt-2"></div>
</div>
<% when :text_line %>
<% count.times do %>
<div class="h-4 bg-darkless rounded animate-pulse mb-2" style="width: <%= rand(60..100) %>%"></div>
<% end %>
<% when :filters %>
<div class="filters-section animate-pulse">
<% 5.times do %>
@ -81,5 +75,4 @@
</div>
<% end %>
</div>
<% end %>

View file

@ -1,17 +1,10 @@
<div class="user-info flex items-center gap-2" title="<%= if user == current_user
FlavorText.same_user.sample
else
user.github_username.presence || user.slack_username.presence
end %>">
<%= image_tag user.avatar_url,
size: "32x32",
class: "rounded-full aspect-square border border-gray-300",
alt: "#{h(user.display_name)}'s avatar" if user.avatar_url %>
<div class="user-info flex items-center gap-2" title="<%= user == current_user ? FlavorText.same_user.sample : user.github_username.presence || user.slack_username.presence %>">
<%= image_tag user.avatar_url, size: '32x32', class: 'rounded-full aspect-square border border-gray-300', alt: "#{h(user.display_name)}'s avatar" if user.avatar_url %>
<span class="inline-flex items-center gap-1">
<% if local_assigns.fetch(:show, []).include?(:profile_link) && user.username.present? %>
<%= link_to h(user.display_name), profile_path(user.username), class: "text-blue-500 hover:underline" %>
<%= link_to h(user.display_name), profile_path(user.username), class: 'text-blue-500 hover:underline' %>
<% elsif local_assigns.fetch(:show, []).include?(:slack) && user.slack_uid.present? %>
<%= link_to "@#{h(user.display_name)}", "https://hackclub.slack.com/team/#{user.slack_uid}", target: "_blank", class: "text-blue-500 hover:underline" %>
<%= link_to "@#{h(user.display_name)}", "https://hackclub.slack.com/team/#{user.slack_uid}", target: '_blank', class: 'text-blue-500 hover:underline' %>
<% else %>
<%= h(user.display_name) %>
<% end %>

View file

@ -1,13 +1,5 @@
<%# This is a partial for a video that plays on hover. it's provided a src %>
<video
class="video-on-hover"
src="<%= src %>"
controls
playsinline
onmouseover="this.play()"
<%# onmouseout="this.pause()" %>>
Your browser does not support the video tag.
</video>
<video class="video-on-hover" src="<%= src %>" controls playsinline onmouseover="this.play()" <%# onmouseout="this.pause()" %>>Your browser does not support the video tag.</video>
<style>
.video-on-hover {

View file

@ -1,9 +1,6 @@
<div class="flex flex-row gap-2 mt-4">
<!-- "nihil boni sine labore" -->
<% hours.each_with_index do |h, i| %>
<div class="bg-white opacity-10 flex-grow min-w-[10px]"
title="<%= pluralize(i + 1, 'hour') %> ago, <%= pluralize(h[:users], 'people') %> logged time. '<%= FlavorText.latin_phrases.sample %>.'"
style="height: <%= h[:height] %>px;">
</div>
<div class="bg-white opacity-10 grow min-w-2.5" title="<%= pluralize(i + 1, 'hour') %> ago, <%= pluralize(h[:users], 'people') %> logged time. '<%= FlavorText.latin_phrases.sample %>.'" style="height: <%= h[:height] %>px;"></div>
<% end %>
</div>

View file

@ -6,34 +6,30 @@
<% duration = daily_durations[date] || 0 %>
<% if duration < 1.minute %>
<% level = 0 %>
<% bg_class = "bg-[#151b23]" %>
<% bg_class = 'bg-[#151b23]' %>
<% else %>
<% ratio = duration.to_f / length_of_busiest_day %>
<% bg_class =
if ratio >= 0.8
"bg-[#56d364]"
elsif ratio >= 0.5
"bg-[#2ea043]"
elsif ratio >= 0.2
"bg-[#196c2e]"
else
"bg-[#033a16]"
end %>
<%
bg_class =
if ratio >= 0.8
'bg-[#56d364]'
elsif ratio >= 0.5
'bg-[#2ea043]'
elsif ratio >= 0.2
'bg-[#196c2e]'
else
'bg-[#033a16]'
end
%>
<% end %>
<a class="day transition-all duration-75 w-3 h-3 rounded-sm hover:scale-110 hover:z-10 hover:shadow-md <%= bg_class %>"
href="?date=<%= date %>"
data-turbo-frame="_top"
data-date="<%= date %>"
data-duration="<%= distance_of_time_in_words(duration) %>"
title="you hacked for <%= distance_of_time_in_words(duration) %> on <%= date %>">
</a>
<a class="day transition-all duration-75 w-3 h-3 rounded-sm hover:scale-110 hover:z-10 hover:shadow-md <%= bg_class %>" href="?date=<%= date %>" data-turbo-frame="_top" data-date="<%= date %>" data-duration="<%= distance_of_time_in_words(duration) %>" title="you hacked for <%= distance_of_time_in_words(duration) %> on <%= date %>"> </a>
<% end %>
</div>
<p class="super">
<% if current_user %>
Calculated in <%= link_to ActiveSupport::TimeZone[current_user.timezone].to_s, my_settings_path(anchor: "user_timezone"), data: { turbo_frame: "_top" } %>
Calculated in <%= link_to ActiveSupport::TimeZone[current_user.timezone].to_s, my_settings_path(anchor: 'user_timezone'), data: { turbo_frame: '_top' } %>
<% else %>
Calculated in <%= ActiveSupport::TimeZone[local_assigns.fetch(:user_tz, "UTC")].to_s %>
Calculated in <%= ActiveSupport::TimeZone[local_assigns.fetch(:user_tz, 'UTC')].to_s %>
<% end %>
</p>
</div>

View file

@ -1,40 +1,11 @@
<%= turbo_frame_tag "filterable_dashboard" do %>
<div class="content">
<div class="filters-section">
<%= render partial: "shared/multi_select", locals: {
label: "Project",
param: "project",
values: @project,
selected: @selected_project
} %>
<%= render partial: "shared/multi_select", locals: {
label: "Language",
param: "language",
values: @language,
selected: @selected_language
} %>
<%= render partial: "shared/multi_select", locals: {
label: "OS",
param: "operating_system",
values: @operating_system,
selected: @selected_operating_system
} %>
<%= render partial: "shared/multi_select", locals: {
label: "Editor",
param: "editor",
values: @editor,
selected: @selected_editor
} %>
<%= render partial: "shared/multi_select", locals: {
label: "Category",
param: "category",
values: @category,
selected: @selected_category
} %>
<%= render partial: "shared/multi_select", locals: { label: "Project", param: "project", values: @project, selected: @selected_project } %>
<%= render partial: "shared/multi_select", locals: { label: "Language", param: "language", values: @language, selected: @selected_language } %>
<%= render partial: "shared/multi_select", locals: { label: "OS", param: "operating_system", values: @operating_system, selected: @selected_operating_system } %>
<%= render partial: "shared/multi_select", locals: { label: "Editor", param: "editor", values: @editor, selected: @selected_editor } %>
<%= render partial: "shared/multi_select", locals: { label: "Category", param: "category", values: @category, selected: @selected_category } %>
</div>
</div>
@ -43,321 +14,324 @@
</div>
<script>
// UI only: handle dropdown behavior, clear buttons, etc.
window.initializeMultiSelect = window.initializeMultiSelect || function(selectId) {
const select = document.getElementById(selectId);
if (!select || select.dataset.initialized) return;
// UI only: handle dropdown behavior, clear buttons, etc.
window.initializeMultiSelect =
window.initializeMultiSelect ||
function (selectId) {
const select = document.getElementById(selectId);
if (!select || select.dataset.initialized) return;
select.dataset.initialized = "true";
const header = select.querySelector(".select-header");
const container = select.querySelector(".options-container");
const checkboxes = select.querySelectorAll('input[type="checkbox"]');
const clearButton = select.querySelector(".clear-button");
const searchInput = select.querySelector(".search-input");
select.dataset.initialized = "true";
const header = select.querySelector(".select-header");
const container = select.querySelector(".options-container");
const checkboxes = select.querySelectorAll('input[type="checkbox"]');
const clearButton = select.querySelector(".clear-button");
const searchInput = select.querySelector(".search-input");
// Header and clear button visibility
const checkedBoxes = Array.from(checkboxes).filter(cb => cb.checked);
if (checkedBoxes.length > 0 && clearButton) {
clearButton.style.display = "block";
if (checkedBoxes.length === 1) {
header.textContent = checkedBoxes[0].value;
} else {
header.textContent = `${checkedBoxes.length} selected`;
}
// Header and clear button visibility
const checkedBoxes = Array.from(checkboxes).filter((cb) => cb.checked);
if (checkedBoxes.length > 0 && clearButton) {
clearButton.style.display = "block";
if (checkedBoxes.length === 1) {
header.textContent = checkedBoxes[0].value;
} else {
header.textContent = `${checkedBoxes.length} selected`;
}
}
// Toggle dropdown
header.addEventListener("click", function(e) {
e.stopPropagation();
const isVisible = container.style.display === "block";
document.querySelectorAll(".options-container").forEach(c => {
if (c !== container) c.style.display = "none";
});
container.style.display = isVisible ? "none" : "block";
if (!isVisible && searchInput) searchInput.focus();
// Toggle dropdown
header.addEventListener("click", function (e) {
e.stopPropagation();
const isVisible = container.style.display === "block";
document.querySelectorAll(".options-container").forEach((c) => {
if (c !== container) c.style.display = "none";
});
container.style.display = isVisible ? "none" : "block";
if (!isVisible && searchInput) searchInput.focus();
});
// Clear filter
if (clearButton) {
clearButton.addEventListener("click", function(e) {
e.stopPropagation();
checkboxes.forEach(cb => cb.checked = false);
updateSelect(select);
});
}
// Clear filter
if (clearButton) {
clearButton.addEventListener("click", function (e) {
e.stopPropagation();
checkboxes.forEach((cb) => (cb.checked = false));
updateSelect(select);
});
}
// Handle search input
if (searchInput) {
searchInput.addEventListener("input", function(e) {
const searchTerm = e.target.value.toLowerCase().trim();
const options = select.querySelectorAll(".option");
options.forEach(option => {
const text = option.querySelector("span").textContent.toLowerCase().trim();
option.style.display = text.includes(searchTerm) ? "" : "none";
});
});
searchInput.addEventListener("click", function(e) {
e.stopPropagation();
});
}
// Handle search input
if (searchInput) {
searchInput.addEventListener("input", function (e) {
const searchTerm = e.target.value.toLowerCase().trim();
const options = select.querySelectorAll(".option");
options.forEach((option) => {
const text = option.querySelector("span").textContent.toLowerCase().trim();
option.style.display = text.includes(searchTerm) ? "" : "none";
});
});
searchInput.addEventListener("click", function (e) {
e.stopPropagation();
});
}
// Update header text and URL when checkboxes change
checkboxes.forEach(checkbox => {
checkbox.addEventListener("change", function() {
updateSelect(select);
});
// Update header text and URL when checkboxes change
checkboxes.forEach((checkbox) => {
checkbox.addEventListener("change", function () {
updateSelect(select);
});
});
};
window.updateSelect =
window.updateSelect ||
function (select) {
const header = select.querySelector(".select-header");
const clearButton = select.querySelector(".clear-button");
const checkboxes = select.querySelectorAll('input[type="checkbox"]');
const param = select.dataset.param;
const frame = document.querySelector("#filterable_dashboard_content");
frame.classList.add("loading");
const selected = Array.from(checkboxes)
.filter((cb) => cb.checked)
.map((cb) => cb.value);
// Header text, clear button
if (selected.length === 0) {
header.textContent = `Filter by ${header.closest(".filter").querySelector(".filter-label").textContent.slice(2).toLowerCase()}...`;
if (clearButton) clearButton.style.display = "none";
} else if (selected.length === 1) {
header.textContent = selected[0];
if (clearButton) clearButton.style.display = "block";
} else {
header.textContent = `${selected.length} selected`;
if (clearButton) clearButton.style.display = "block";
}
// Update URL params
const rootUrl = new URL(window.location);
if (selected.length > 0) {
rootUrl.searchParams.set(param, selected.join(","));
} else {
rootUrl.searchParams.delete(param);
}
window.history.pushState({}, "", rootUrl);
// update content-frame url for Turbo
const contentUrl = new URL(window.location);
contentUrl.pathname = <%= filterable_dashboard_content_static_pages_path.to_json %>;
contentUrl.searchParams.set(param, selected.join(","));
frame.src = contentUrl.toString();
const requestTimestamp = Date.now();
window.lastRequestTimestamp = requestTimestamp;
fetch(contentUrl.toString(), {
headers: { Accept: "text/html" },
})
.then((response) => response.text())
.then((html) => {
if (requestTimestamp === window.lastRequestTimestamp) {
frame.innerHTML = html;
frame.classList.remove("loading");
window.hackatimeCharts?.initializeCharts();
}
});
};
window.updateSelect = window.updateSelect || function(select) {
const header = select.querySelector(".select-header");
const clearButton = select.querySelector(".clear-button");
const checkboxes = select.querySelectorAll('input[type="checkbox"]');
const param = select.dataset.param;
const frame = document.querySelector("#filterable_dashboard_content");
frame.classList.add("loading");
const selected = Array.from(checkboxes)
.filter(cb => cb.checked)
.map(cb => cb.value);
// Header text, clear button
if (selected.length === 0) {
header.textContent = `Filter by ${header.closest(".filter").querySelector(".filter-label").textContent.slice(2).toLowerCase()}...`;
if (clearButton) clearButton.style.display = "none";
} else if (selected.length === 1) {
header.textContent = selected[0];
if (clearButton) clearButton.style.display = "block";
} else {
header.textContent = `${selected.length} selected`;
if (clearButton) clearButton.style.display = "block";
document.addEventListener("turbo:frame-load", function (event) {
if (event.target.id === "filterable_dashboard") {
["project", "language", "editor", "operating_system", "category"].forEach((type) => {
window.initializeMultiSelect(`${type}-select`);
});
document.addEventListener("click", function (e) {
if (!e.target.closest(".custom-select")) {
document.querySelectorAll(".options-container").forEach((container) => {
container.style.display = "none";
});
}
// Update URL params
const rootUrl = new URL(window.location);
if (selected.length > 0) {
rootUrl.searchParams.set(param, selected.join(","));
} else {
rootUrl.searchParams.delete(param);
}
window.history.pushState({}, "", rootUrl);
// update content-frame url for Turbo
const contentUrl = new URL(window.location);
contentUrl.pathname = <%== filterable_dashboard_content_static_pages_path.to_json %>;
contentUrl.searchParams.set(param, selected.join(","));
frame.src = contentUrl.toString();
const requestTimestamp = Date.now();
window.lastRequestTimestamp = requestTimestamp;
fetch(contentUrl.toString(), {
headers: { "Accept": "text/html" }
}).then(response => response.text()).then(html => {
if (requestTimestamp === window.lastRequestTimestamp) {
frame.innerHTML = html;
frame.classList.remove("loading");
window.hackatimeCharts?.initializeCharts();
}
});
};
document.addEventListener("turbo:frame-load", function(event) {
if (event.target.id === "filterable_dashboard") {
["project", "language", "editor", "operating_system", "category"].forEach(type => {
window.initializeMultiSelect(`${type}-select`);
});
document.addEventListener("click", function(e) {
if (!e.target.closest(".custom-select")) {
document.querySelectorAll(".options-container").forEach(container => {
container.style.display = "none";
});
}
});
}
});
});
}
});
</script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" data-turbo-track="reload"></script>
<script>
window.chartInstances = window.chartInstances || {};
window.chartInstances = window.chartInstances || {};
if (!window.hackatimeCharts) {
window.hackatimeCharts = {
formatDuration(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) { return `${hours}h ${minutes}m`; }
else { return `${minutes}m`; }
if (!window.hackatimeCharts) {
window.hackatimeCharts = {
formatDuration(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h ${minutes}m`;
} else {
return `${minutes}m`;
}
},
createPieChart(elementId) {
const canvas = document.getElementById(elementId);
if (!canvas) return;
const stats = JSON.parse(canvas.dataset.stats);
const labels = Object.keys(stats);
const data = Object.values(stats);
if (window.chartInstances[elementId]) {
window.chartInstances[elementId].destroy();
}
const ctx = canvas.getContext("2d");
const pieColors = ["#60a5fa", "#f472b6", "#fb923c", "#facc15", "#4ade80", "#2dd4bf", "#a78bfa", "#f87171", "#38bdf8", "#e879f9", "#34d399", "#fbbf24", "#818cf8", "#fb7185", "#22d3ee", "#a3e635", "#c084fc", "#f97316", "#14b8a6", "#8b5cf6", "#ec4899", "#84cc16", "#06b6d4", "#d946ef", "#10b981"];
const backgroundColors = labels.map((_, i) => pieColors[i % pieColors.length]);
window.chartInstances[elementId] = new Chart(ctx, {
type: "pie",
data: { labels, datasets: [{ data, backgroundColor: backgroundColors, borderWidth: 1 }] },
options: {
responsive: true,
maintainAspectRatio: true,
aspectRatio: 1.2,
plugins: {
tooltip: {
callbacks: {
label: function (context) {
const label = context.label || "";
const value = context.raw || 0;
const duration = window.hackatimeCharts.formatDuration(value);
const percentage = ((value / data.reduce((a, b) => a + b, 0)) * 100).toFixed(1);
return `${label}: ${duration} (${percentage}%)`;
},
},
},
legend: {
position: "right",
align: "center",
labels: {
boxWidth: 10,
padding: 8,
font: { size: 10 },
},
},
},
createPieChart(elementId) {
const canvas = document.getElementById(elementId);
if (!canvas) return;
const stats = JSON.parse(canvas.dataset.stats);
const labels = Object.keys(stats);
const data = Object.values(stats);
if (window.chartInstances[elementId]) {
window.chartInstances[elementId].destroy();
}
const ctx = canvas.getContext("2d");
const pieColors = [
'#60a5fa', '#f472b6', '#fb923c', '#facc15', '#4ade80',
'#2dd4bf', '#a78bfa', '#f87171', '#38bdf8', '#e879f9',
'#34d399', '#fbbf24', '#818cf8', '#fb7185', '#22d3ee',
'#a3e635', '#c084fc', '#f97316', '#14b8a6', '#8b5cf6',
'#ec4899', '#84cc16', '#06b6d4', '#d946ef', '#10b981'
];
const backgroundColors = labels.map((_, i) => pieColors[i % pieColors.length]);
window.chartInstances[elementId] = new Chart(ctx, {
type: "pie",
data: { labels, datasets: [{data, backgroundColor: backgroundColors, borderWidth: 1 }] },
options: {
responsive: true,
maintainAspectRatio: true,
aspectRatio: 1.2,
plugins: {
tooltip: {
callbacks: {
label: function(context) {
const label = context.label || "";
const value = context.raw || 0;
const duration = window.hackatimeCharts.formatDuration(value);
const percentage = ((value / data.reduce((a, b) => a + b, 0)) * 100).toFixed(1);
return `${label}: ${duration} (${percentage}%)`;
}
}
},
legend: {
position: "right",
align: "center",
labels: {
boxWidth: 10,
padding: 8,
font: { size: 10 }
}
}
}
}
});
},
createProjectTimelineChart() {
const canvas = document.getElementById("projectTimelineChart");
if (!canvas) return;
const weeklyStats = JSON.parse(canvas.dataset.stats);
const allProjects = new Set();
Object.values(weeklyStats).forEach(weekData => {
Object.keys(weekData).forEach(project => allProjects.add(project));
});
const sortedWeeks = Object.keys(weeklyStats).sort();
const datasets = Array.from(allProjects).map((project) => ({
label: project,
data: sortedWeeks.map(week => weeklyStats[week][project] || 0),
stack: "stack0",
}));
datasets.sort((a, b) => {
const sumA = a.data.reduce((acc, val) => acc + val, 0);
const sumB = b.data.reduce((acc, val) => acc + val, 0);
return sumB - sumA;
});
if (window.chartInstances["projectTimelineChart"]) {
window.chartInstances["projectTimelineChart"].destroy();
}
const ctx = canvas.getContext("2d");
window.chartInstances["projectTimelineChart"] = new Chart(ctx, {
type: "bar",
data: {
labels: sortedWeeks.map(week => {
const date = new Date(week);
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}),
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true,
grid: { display: false }
},
y: {
stacked: true,
type: "linear",
grid: {
color: (ctx) => {
if (ctx.tick.value === 0) return "transparent";
return ctx.tick.value % 1 === 0 ? "rgba(0, 0, 0, 0.1)" : "rgba(0, 0, 0, 0.05)";
}
},
ticks: {
callback: function(value) {
if (value === 0) return "0s";
const hours = Math.floor(value / 3600);
const minutes = Math.floor((value % 3600) / 60);
if (hours > 0) return `${hours}h`;
return `${minutes}m`;
}
}
}
},
plugins: {
legend: {
position: "right",
labels: {
boxWidth: 12,
padding: 15
}
},
tooltip: {
callbacks: {
label: function(context) {
const value = context.raw;
const hours = Math.floor(value / 3600);
const minutes = Math.floor((value % 3600) / 60);
if (hours > 0) {
return `${context.dataset.label}: ${hours}h ${minutes}m`;
}
return `${context.dataset.label}: ${minutes}m`;
}
}
}
}
}
});
},
initializeCharts() {
this.createPieChart("languageChart");
this.createPieChart("editorChart");
this.createPieChart("operatingSystemChart");
this.createProjectTimelineChart();
}
};
}
if (!window.chartListenersInitialized) {
window.chartListenersInitialized = true;
document.addEventListener("turbo:frame-load", () => {
if (typeof Chart === "undefined") {
const checkChart = setInterval(() => {
if (typeof Chart !== "undefined") {
clearInterval(checkChart);
window.hackatimeCharts.initializeCharts();
}
}, 50);
setTimeout(() => clearInterval(checkChart), 5000);
} else {
window.hackatimeCharts.initializeCharts();
}
},
});
}
if (typeof Chart !== "undefined") {
},
createProjectTimelineChart() {
const canvas = document.getElementById("projectTimelineChart");
if (!canvas) return;
const weeklyStats = JSON.parse(canvas.dataset.stats);
const allProjects = new Set();
Object.values(weeklyStats).forEach((weekData) => {
Object.keys(weekData).forEach((project) => allProjects.add(project));
});
const sortedWeeks = Object.keys(weeklyStats).sort();
const datasets = Array.from(allProjects).map((project) => ({
label: project,
data: sortedWeeks.map((week) => weeklyStats[week][project] || 0),
stack: "stack0",
}));
datasets.sort((a, b) => {
const sumA = a.data.reduce((acc, val) => acc + val, 0);
const sumB = b.data.reduce((acc, val) => acc + val, 0);
return sumB - sumA;
});
if (window.chartInstances["projectTimelineChart"]) {
window.chartInstances["projectTimelineChart"].destroy();
}
const ctx = canvas.getContext("2d");
window.chartInstances["projectTimelineChart"] = new Chart(ctx, {
type: "bar",
data: {
labels: sortedWeeks.map((week) => {
const date = new Date(week);
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}),
datasets: datasets,
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true,
grid: { display: false },
},
y: {
stacked: true,
type: "linear",
grid: {
color: (ctx) => {
if (ctx.tick.value === 0) return "transparent";
return ctx.tick.value % 1 === 0 ? "rgba(0, 0, 0, 0.1)" : "rgba(0, 0, 0, 0.05)";
},
},
ticks: {
callback: function (value) {
if (value === 0) return "0s";
const hours = Math.floor(value / 3600);
const minutes = Math.floor((value % 3600) / 60);
if (hours > 0) return `${hours}h`;
return `${minutes}m`;
},
},
},
},
plugins: {
legend: {
position: "right",
labels: {
boxWidth: 12,
padding: 15,
},
},
tooltip: {
callbacks: {
label: function (context) {
const value = context.raw;
const hours = Math.floor(value / 3600);
const minutes = Math.floor((value % 3600) / 60);
if (hours > 0) {
return `${context.dataset.label}: ${hours}h ${minutes}m`;
}
return `${context.dataset.label}: ${minutes}m`;
},
},
},
},
},
});
},
initializeCharts() {
this.createPieChart("languageChart");
this.createPieChart("editorChart");
this.createPieChart("operatingSystemChart");
this.createProjectTimelineChart();
},
};
}
if (!window.chartListenersInitialized) {
window.chartListenersInitialized = true;
document.addEventListener("turbo:frame-load", () => {
if (typeof Chart === "undefined") {
const checkChart = setInterval(() => {
if (typeof Chart !== "undefined") {
clearInterval(checkChart);
window.hackatimeCharts.initializeCharts();
}
}, 50);
setTimeout(() => clearInterval(checkChart), 5000);
} else {
window.hackatimeCharts.initializeCharts();
}
}
});
}
if (typeof Chart !== "undefined") {
window.hackatimeCharts.initializeCharts();
}
</script>
<% end %>

View file

@ -57,45 +57,44 @@
<div class="dashboard-grid">
<% if @project_durations&.size&.> 1 %>
<div class="card">
<h2>Project Durations</h2>
<div class="bar-graph">
<%
max_duration = @project_durations.values.max
min_duration = @project_durations.values.min
<div class="card">
<h2>Project Durations</h2>
<div class="bar-graph">
<%
max_duration = @project_durations.values.max
# Use logarithmic scale for better visibility of smaller values
# Add 1 to avoid log(0), scale to 15-100 range
def log_scale(value, max_val)
return 0 if value == 0
min_percent = 5 # Minimum bar width percentage
max_percent = 100 # Maximum bar width percentage
# Use logarithmic scale for better visibility of smaller values
# Add 1 to avoid log(0), scale to 15-100 range
def log_scale(value, max_val)
return 0 if value == 0
min_percent = 5 # Minimum bar width percentage
max_percent = 100 # Maximum bar width percentage
# Mix linear and logarithmic scaling
# 80% linear, 20% logarithmic
linear_ratio = value.to_f / max_val
log_ratio = Math.log(value + 1) / Math.log(max_val + 1)
# Mix linear and logarithmic scaling
# 80% linear, 20% logarithmic
linear_ratio = value.to_f / max_val
log_ratio = Math.log(value + 1) / Math.log(max_val + 1)
linear_weight = 0.8
log_weight = 0.2
linear_weight = 0.8
log_weight = 0.2
scaled = min_percent + (linear_weight * linear_ratio + log_weight * log_ratio) * (max_percent - min_percent)
[scaled, max_percent].min.round
end
%>
scaled = min_percent + (linear_weight * linear_ratio + log_weight * log_ratio) * (max_percent - min_percent)
[scaled, max_percent].min.round
end
%>
<% @project_durations.each do |project, duration| %>
<div class="bar-row">
<div class="bar-label"><%= h(project.presence || "Unknown") %></div>
<div class="bar-container">
<div class="bar" style="width: <%= log_scale(duration, max_duration) %>%">
<span class="bar-value"><%= ApplicationController.helpers.short_time_simple(duration) %></span>
<% @project_durations.each do |project, duration| %>
<div class="bar-row">
<div class="bar-label"><%= h(project.presence || 'Unknown') %></div>
<div class="bar-container">
<div class="bar" style="width: <%= log_scale(duration, max_duration) %>%">
<span class="bar-value"><%= ApplicationController.helpers.short_time_simple(duration) %></span>
</div>
</div>
</div>
</div>
<% end %>
<% end %>
</div>
</div>
</div>
<% end %>
<%# Language distribution %>

View file

@ -1,31 +1,29 @@
<%= turbo_frame_tag "project_durations" do %>
<%
max = project_durations.map { |p| p[:duration] }.max || 1
%>
<% max = project_durations.map { |p| p[:duration] }.max || 1 %>
<p class="text-lg text-white mt-6">
<% total_time = project_durations.sum { |p| p[:duration].to_i } %>
<% total_time = project_durations.sum { |p| p[:duration].to_i } %>
<% if total_time > 0 %>
You've spent <span class="font-semibold"><%= short_time_detailed(total_time) %></span> coding across all projects.
<% else %>
You haven't logged any time yet. Start coding!
<% end %>
<% if total_time > 0 %>
You've spent <span class="font-semibold"><%= short_time_detailed(total_time) %></span> coding across all projects.
<% else %>
You haven't logged any time yet. Start coding!
<% end %>
</p>
<div class="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-6 mt-6">
<% project_durations.each do |project| %>
<% if current_user.github_uid.present? && project[:project].present? %>
<%= render "my/project_repo_mappings/edit_modal", project: project %>
<%= render 'my/project_repo_mappings/edit_modal', project: project %>
<% if show_archived %>
<%= render "my/project_repo_mappings/unarchive_modal", project: project %>
<%= render 'my/project_repo_mappings/unarchive_modal', project: project %>
<% else %>
<%= render "my/project_repo_mappings/archive_modal", project: project %>
<%= render 'my/project_repo_mappings/archive_modal', project: project %>
<% end %>
<% end %>
<div class="bg-dark border border-primary rounded-xl p-6 shadow-lg transition-all duration-300 flex flex-col gap-4 hover:border-primary/40 hover:-translate-y-1 hover:shadow-xl backdrop-blur-sm">
<div class="flex justify-between items-start gap-3">
<div class="flex flex-col gap-2 flex-1 min-w-0">
<h3 class="text-lg font-semibold text-white truncate" title="<%= h(project[:project]) %>">
<%= h(project[:project].presence || "Unknown") %>
<%= h(project[:project].presence || 'Unknown') %>
</h3>
<% if project[:repository]&.stars.present? %>
<div class="flex items-center gap-1 text-sm text-yellow-400">
@ -53,30 +51,21 @@
<% end %>
<% if current_user.github_uid.present? && project[:project].present? %>
<% modal_id = "edit-project-modal-#{project[:project]&.parameterize || 'unknown'}" %>
<button type="button"
onclick="document.getElementById(<%= modal_id.to_json %>).dispatchEvent(new CustomEvent('modal:open'))"
class="p-2 rounded-lg bg-white/5 hover:bg-white/10 transition-colors duration-200 cursor-pointer"
title="Edit mapping">
<button type="button" onclick="document.getElementById(<%= modal_id.to_json %>).dispatchEvent(new CustomEvent('modal:open'))" class="p-2 rounded-lg bg-white/5 hover:bg-white/10 transition-colors duration-200 cursor-pointer" title="Edit mapping">
<svg class="w-4 h-4 text-white/70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path fill="currentColor" d="M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83l3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75z" />
</svg>
</button>
<% if show_archived %>
<% unarchive_modal_id = "unarchive-project-modal-#{project[:project]&.parameterize || 'unknown'}" %>
<button type="button"
onclick="document.getElementById(<%= unarchive_modal_id.to_json %>).dispatchEvent(new CustomEvent('modal:open'))"
class="p-2 rounded-lg bg-white/5 hover:bg-white/10 transition-colors duration-200 cursor-pointer"
title="Restore project">
<button type="button" onclick="document.getElementById(<%= unarchive_modal_id.to_json %>).dispatchEvent(new CustomEvent('modal:open'))" class="p-2 rounded-lg bg-white/5 hover:bg-white/10 transition-colors duration-200 cursor-pointer" title="Restore project">
<svg class="w-4 h-4 text-white/70" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="m20.55 5.22l-1.39-1.68A1.51 1.51 0 0 0 18 3H6c-.47 0-.88.21-1.15.55L3.46 5.22C3.17 5.57 3 6.01 3 6.5V19a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V6.5c0-.49-.17-.93-.45-1.28m-8.2 4.63L17.5 15H14v2h-4v-2H6.5l5.15-5.15c.19-.19.51-.19.7 0M5.12 5l.82-1h12l.93 1z" />
</svg>
</button>
<% else %>
<% archive_modal_id = "archive-project-modal-#{project[:project]&.parameterize || 'unknown'}" %>
<button type="button"
onclick="document.getElementById(<%= archive_modal_id.to_json %>).dispatchEvent(new CustomEvent('modal:open'))"
class="p-2 rounded-lg bg-white/5 hover:bg-white/10 transition-colors duration-200 cursor-pointer"
title="Archive project">
<button type="button" onclick="document.getElementById(<%= archive_modal_id.to_json %>).dispatchEvent(new CustomEvent('modal:open'))" class="p-2 rounded-lg bg-white/5 hover:bg-white/10 transition-colors duration-200 cursor-pointer" title="Archive project">
<svg class="w-4 h-4 text-white/70" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="m20.54 5.23l-1.39-1.68C18.88 3.21 18.47 3 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6.02 3 6.5V19c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6.5c0-.48-.17-.93-.46-1.27m-8.89 11.92L6.5 12H10v-2h4v2h3.5l-5.15 5.15c-.19.19-.51.19-.7 0M5.12 5l.81-1h12l.94 1z" />
</svg>
@ -90,9 +79,7 @@
<span class="text-2xl font-bold text-primary"><%= short_time_detailed project[:duration] %></span>
<div class="flex items-center gap-2 hidden">
<div class="w-16 h-2 bg-white/10 rounded-full overflow-hidden">
<div
class="h-full bg-primary transition-all duration-500 rounded-full"
style="width: <%= [project[:duration].to_f / max.to_f * 100, 100].min %>%;"></div>
<div class="h-full bg-primary transition-all duration-500 rounded-full" style="width: <%= [project[:duration].to_f / max.to_f * 100, 100].min %>%;"></div>
</div>
<span class="text-xs text-white/40"><%= number_to_percentage([project[:duration].to_f / max.to_f * 100, 100].min, precision: 0) %></span>
</div>

View file

@ -17,8 +17,7 @@
<%= @flavor_text %>
</p>
</div>
<div id="clock" class="clockicons clock-display">
</div>
<div id="clock" class="clockicons clock-display"></div>
<% if current_user %>
<h1 class="font-bold mt-1 mb-4 text-5xl text-center">Keep Track of <span class="text-primary">Your</span> Coding Time</h1>
<% else %>
@ -46,36 +45,25 @@
</div>
<% end %>
<%= link_to slack_auth_path, class: "flex items-center justify-center px-4 py-3 rounded text-white cursor-pointer bg-dark hover:bg-darkless border border-darkless text-gray-300 transition-colors w-1/4 gap-2" do %>
<span><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-6 h-6"><path fill="currentColor" d="M6 15a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2h2zm1 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2a2 2 0 0 1-2-2zm2-8a2 2 0 0 1-2-2a2 2 0 0 1 2-2a2 2 0 0 1 2 2v2zm0 1a2 2 0 0 1 2 2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2a2 2 0 0 1 2-2zm8 2a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2h-2zm-1 0a2 2 0 0 1-2 2a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2a2 2 0 0 1 2 2zm-2 8a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2v-2zm0-1a2 2 0 0 1-2-2a2 2 0 0 1 2-2h5a2 2 0 0 1 2 2a2 2 0 0 1-2 2z" /></svg></span>
<span class="hidden xl:inline">Slack Sign In</span>
<span><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-6 h-6"><path fill="currentColor" d="M6 15a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2h2zm1 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2a2 2 0 0 1-2-2zm2-8a2 2 0 0 1-2-2a2 2 0 0 1 2-2a2 2 0 0 1 2 2v2zm0 1a2 2 0 0 1 2 2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2a2 2 0 0 1 2-2zm8 2a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2h-2zm-1 0a2 2 0 0 1-2 2a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2a2 2 0 0 1 2 2zm-2 8a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2v-2zm0-1a2 2 0 0 1-2-2a2 2 0 0 1 2-2h5a2 2 0 0 1 2 2a2 2 0 0 1-2 2z" /></svg></span>
<span class="hidden xl:inline">Slack Sign In</span>
<% end %>
</div>
</div>
<% if params[:sign_in_email] %>
<div class="text-green-500 mt-4 text-center max-w-[50vw] mx-auto">
Check your email for a sign-in link!
</div>
<div class="text-green-500 mt-4 text-center max-w-[50vw] mx-auto">Check your email for a sign-in link!</div>
<% dev_tool class: "text-center max-w-[50vw] mx-auto mb-4" do %>
Because you're on localhost, <%= link_to "click here to view the email", letter_opener_web_path %>
<% end %>
<% end %>
<div class="w-full flex justify-center overflow-x-none">
<p class="monospace text-center text-primary text-[22px] select-none whitespace-nowrap">
==============================================/ h a c k /=============================================
</p>
<p class="monospace text-center text-primary text-[22px] select-none whitespace-nowrap">==============================================/ h a c k /=============================================</p>
</div>
<div class="mt-8 mb-8">
<h1 class="font-bold mt-1 mb-1 text-4xl">Compatible with your favourite IDEs</h1>
<p class="text-primary monospace text-[20px]">Hackatime works with these code editors and more!</p>
<div id="supported-editors" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4 mt-4">
<% popular_editors = [
['VS Code', 'vs-code'], ['PyCharm', 'pycharm'], ['IntelliJ IDEA', 'intellij-idea'],
['Sublime Text', 'sublime-text'], ['Vim', 'vim'], ['Neovim', 'neovim'],
['Android Studio', 'android-studio'], ['Xcode', 'xcode'], ['Unity', 'unity'],
['Godot', 'godot'], ['Cursor', 'cursor'], ['Zed', 'zed'],
['Terminal', 'terminal'], ['WebStorm', 'webstorm'], ['Eclipse', 'eclipse'],
['Emacs', 'emacs'], ['Jupyter', 'jupyter'], ['OnShape', 'onshape']
] %>
<% popular_editors = [['VS Code', 'vs-code'], %w[PyCharm pycharm], ['IntelliJ IDEA', 'intellij-idea'], ['Sublime Text', 'sublime-text'], %w[Vim vim], %w[Neovim neovim], ['Android Studio', 'android-studio'], %w[Xcode xcode], %w[Unity unity], %w[Godot godot], %w[Cursor cursor], %w[Zed zed], %w[Terminal terminal], %w[WebStorm webstorm], %w[Eclipse eclipse], %w[Emacs emacs], %w[Jupyter jupyter], %w[OnShape onshape]] %>
<% popular_editors.each do |name, slug| %>
<a href="<%= doc_path("editors/#{slug}") %>" class="bg-darkless rounded-lg p-3 hover:bg-primary/20 transition-all duration-200 text-center block hover:-translate-y-0.5 hover:shadow-lg hover:shadow-primary/20">
<img src="/images/editor-icons/<%= slug %>-128.png" alt="<%= name %>" class="w-12 h-12 mx-auto mb-2">
@ -85,15 +73,13 @@
</div>
</div>
<div class="w-full flex justify-center overflow-x-none">
<p class="monospace text-center text-primary text-[22px] select-none whitespace-nowrap">
-----------------------------------------------------------------------------------------------
</p>
<p class="monospace text-center text-primary text-[22px] select-none whitespace-nowrap">-----------------------------------------------------------------------------------------------</p>
</div>
<div class="mt-8 mb-8">
<h1 class="font-bold mt-1 mb-1 text-4xl">Why Hackatime?</h1>
<% if @home_stats&.[](:seconds_tracked) && @home_stats&.[](:users_tracked) %>
<p class="text-primary monospace text-[20px]">
We've tracked over <span class="text-primary"><%= number_with_delimiter(@home_stats[:seconds_tracked] / 3600) %> <%= 'hour'.pluralize(@home_stats[:seconds_tracked] / 3600) %></span> of coding time across <span class="text-primary"><%= number_with_delimiter(@home_stats[:users_tracked]) %> <%= 'high schooler'.pluralize(@home_stats[:users_tracked]) %></span> since <span class="text-primary">2025</span>!
We've tracked over <span class="text-primary"><%= number_with_delimiter(@home_stats[:seconds_tracked] / 3600) %> <%= 'hour'.pluralize(@home_stats[:seconds_tracked] / 3600) %> </span> of coding time across <span class="text-primary"><%= number_with_delimiter(@home_stats[:users_tracked]) %> <%= 'high schooler'.pluralize(@home_stats[:users_tracked]) %> </span> since <span class="text-primary">2025</span>!
</p>
<% end %>
<div class="overflow-x-auto -mx-4 px-4 pb-4 no-scrollbar">
@ -106,10 +92,7 @@
</div>
</div>
<div class="w-full flex justify-center overflow-x-none">
<p class="monospace text-center text-primary text-[22px] select-none whitespace-nowrap">
--------------------------------| #hackclub | #hackclub | #hackclub |--------------------------------
</p>
<p class="monospace text-center text-primary text-[22px] select-none whitespace-nowrap">--------------------------------| #hackclub | #hackclub | #hackclub |--------------------------------</p>
</div>
<div class="mt-8 mb-8">
<h1 class="font-bold mt-1 mb-1 text-4xl">For your favorite <span class="text-primary">Hack Club</span> events!</h1>
@ -130,7 +113,7 @@
</div>
</div>
</div>
<div class="flex flex-col md:flex-row bg-gradient-to-r from-[#EFCCCC] to-[#D35648] mt-4 mb-4 rounded-lg">
<div class="flex flex-col md:flex-row bg-linear-to-r from-[#EFCCCC] to-[#D35648] mt-4 mb-4 rounded-lg">
<div class="w-full md:w-1/3 -translate-y-5">
<img src="/images/athena.png" class="w-[400px]">
</div>
@ -145,9 +128,7 @@
<% if current_user %>
<% if @show_wakatime_setup_notice %>
<div class="text-left my-8 flex flex-col">
<p class="mb-4 text-xl text-primary">
Hello friend! Looks like you are new around here, let's get you set up so you can start tracking your coding time.
</p>
<p class="mb-4 text-xl text-primary">Hello friend! Looks like you are new around here, let's get you set up so you can start tracking your coding time.</p>
<%= link_to "Let's setup Hackatime! Click me :D", my_wakatime_setup_path, class: "inline-block w-auto text-3xl font-bold px-8 py-4 bg-primary text-white rounded shadow-md hover:shadow-lg hover:-translate-y-1 transition-all duration-300 animate-pulse" %>
<div class="flex items-center mt-4 flex-nowrap">
<% if @ssp_users_recent&.any? %>
@ -230,7 +211,7 @@
<%= render "static_pages/filterable_dashboard_loading" %>
<% end %>
<%= turbo_frame_tag "activity_graph", src: activity_graph_static_pages_path, loading: :lazy do %>
<%= render "static_pages/activity_graph_loading" %>
<%= render 'static_pages/activity_graph_loading' %>
<% end %>
<% else %>
<% if @leaderboard %>
@ -239,9 +220,7 @@
<% end %>
<div class="w-full flex justify-center overflow-x-none">
<p class="monospace text-center text-primary text-[22px] select-none whitespace-nowrap">
==============================================/ h a c k /=============================================
</p>
<p class="monospace text-center text-primary text-[22px] select-none whitespace-nowrap">==============================================/ h a c k /=============================================</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 my-8 items-center">

View file

@ -1,4 +1,4 @@
<% content_for(:body_class) { "!p-0 !mb-0" } %>
<% content_for(:body_class) { '!p-0 !mb-0' } %>
<div class="fixed inset-0 flex items-center justify-center bg-darker">
<div class="w-full max-w-md px-6 py-8">
@ -7,7 +7,7 @@
</header>
<%= link_to hca_auth_path(continue: @continue_param), class: "inline-flex items-center justify-center w-full px-6 py-3 rounded text-white font-bold bg-primary hover:bg-primary/75 transition-colors", data: { turbo: false }, onclick: "let s=this.querySelector('.spinner'),i=this.querySelector('.icon');s.classList.remove('hidden');i.classList.add('hidden');this.style.cssText='pointer-events:none;opacity:0.7'" do %>
<span class="spinner mr-2 hidden"><%= render "shared/spinner", class: "h-6 w-6" %></span>
<span class="spinner mr-2 hidden"><%= render 'shared/spinner', class: 'h-6 w-6' %></span>
<img src="/images/icon-rounded.png" class="icon h-6 w-6 mr-2">
<span>Sign in with your Hack Club account</span>
<% end %>
@ -21,7 +21,7 @@
<%= form_tag email_auth_path, class: "relative", data: { turbo: false } do %>
<%= hidden_field_tag :continue, @continue_param if @continue_param %>
<div class="relative">
<%= email_field_tag :email, params[:email], placeholder: "Enter your email", required: true, class: "w-full px-3 py-3 pr-12 border border-darkless bg-dark placeholder-secondary rounded focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500" %>
<%= email_field_tag :email, params[:email], placeholder: 'Enter your email', required: true, class: 'w-full px-3 py-3 pr-12 border border-darkless bg-dark placeholder-secondary rounded focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500' %>
<button type="submit" class="absolute right-2 top-1/2 transform -translate-y-1/2 w-8 h-8 p-1 bg-blue-600 hover:bg-blue-700 rounded cursor-pointer border-none flex items-center justify-center transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-5 h-5"><path fill="currentColor" d="M13.3 20.275q-.3-.3-.3-.7t.3-.7L16.175 16H7q-.825 0-1.412-.587T5 14V5q0-.425.288-.712T6 4t.713.288T7 5v9h9.175l-2.9-2.9q-.3-.3-.288-.7t.288-.7q.3-.3.7-.312t.7.287L19.3 14.3q.15.15.212.325t.063.375t-.063.375t-.212.325l-4.575 4.575q-.3.3-.712.3t-.713-.3" /></svg>
</button>

View file

@ -1,61 +1,51 @@
<% content_for :head do %>
<!-- Article Schema -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "What is Hackatime?",
"description": "Hackatime is a free, open-source coding time tracker built by Hack Club for high school students. Track your programming time across 75+ editors and see your coding statistics.",
"url": "https://hackatime.hackclub.com/what-is-hackatime",
"datePublished": "2025-01-01",
"dateModified": "2025-01-01",
"author": {
"@type": "Organization",
"name": "Hack Club",
"url": "https://hackclub.com"
},
"publisher": {
"@type": "Organization",
"name": "Hack Club",
"url": "https://hackclub.com"
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://hackatime.hackclub.com/what-is-hackatime"
},
"about": {
"@type": "SoftwareApplication",
"name": "Hackatime",
"description": "Free and open source coding time tracker by Hack Club"
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "What is Hackatime?",
"description": "Hackatime is a free, open-source coding time tracker built by Hack Club for high school students. Track your programming time across 75+ editors and see your coding statistics.",
"url": "https://hackatime.hackclub.com/what-is-hackatime",
"datePublished": "2025-01-01",
"dateModified": "2025-01-01",
"author": {
"@type": "Organization",
"name": "Hack Club",
"url": "https://hackclub.com"
},
"publisher": {
"@type": "Organization",
"name": "Hack Club",
"url": "https://hackclub.com"
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://hackatime.hackclub.com/what-is-hackatime"
},
"about": {
"@type": "SoftwareApplication",
"name": "Hackatime",
"description": "Free and open source coding time tracker by Hack Club"
}
}
}
</script>
<% end %>
<div class="container">
<div class="max-w-4xl mx-auto">
<h1 class="text-4xl font-bold mb-6 text-center">
What is <span class="text-primary">Hackatime</span>?
</h1>
<h1 class="text-4xl font-bold mb-6 text-center">What is <span class="text-primary">Hackatime</span>?</h1>
<div class="bg-dark rounded-lg p-8 mb-8">
<p class="text-lg mb-6">
<strong class="text-primary">Hackatime</strong> is a free, open-source coding time tracker built by <a href="https://hackclub.com" target="_blank" class="text-primary hover:text-red underline">Hack Club</a> for high school students and developers who want to understand their programming habits.
</p>
<p class="text-lg mb-6"><strong class="text-primary">Hackatime</strong> is a free, open-source coding time tracker built by <a href="https://hackclub.com" target="_blank" class="text-primary hover:text-red underline">Hack Club</a> for high school students and developers who want to understand their programming habits.</p>
<p class="text-lg mb-6">
Unlike other time tracking tools, <strong>Hackatime</strong> is completely free and designed specifically for the Hack Club community. It helps you see exactly how much time you spend coding, which programming languages you use most, and which editors you prefer.
</p>
<p class="text-lg mb-6">Unlike other time tracking tools, <strong>Hackatime</strong> is completely free and designed specifically for the Hack Club community. It helps you see exactly how much time you spend coding, which programming languages you use most, and which editors you prefer.</p>
<h2 class="text-2xl font-semibold text-primary mb-4">How Hackatime Works</h2>
<p class="text-lg mb-6">
<strong>Hackatime</strong> tracks your coding activity automatically by monitoring when you're actively typing in your code editor. It works with over 75 different editors including VS Code, JetBrains IDEs, vim, emacs, and many more.
</p>
<p class="text-lg mb-6"><strong>Hackatime</strong> tracks your coding activity automatically by monitoring when you're actively typing in your code editor. It works with over 75 different editors including VS Code, JetBrains IDEs, vim, emacs, and many more.</p>
<h2 class="text-2xl font-semibold text-primary mb-4">Why Hackatime Exists</h2>
<p class="text-lg mb-6">
<strong>Hackatime</strong> was created because Hack Club believes that the more time you spend making things, the better you get at building cool projects. By tracking your coding time, you can see your progress and stay motivated to keep building.
</p>
<p class="text-lg mb-6"><strong>Hackatime</strong> was created because Hack Club believes that the more time you spend making things, the better you get at building cool projects. By tracking your coding time, you can see your progress and stay motivated to keep building.</p>
<h2 class="text-2xl font-semibold text-primary mb-4">Key Features of Hackatime</h2>
<ul class="list-disc list-inside text-lg mb-6 space-y-2">
@ -68,20 +58,15 @@
</ul>
<h2 class="text-2xl font-semibold text-primary mb-4">Getting Started with Hackatime</h2>
<p class="text-lg mb-6">
To start using <strong>Hackatime</strong>, simply sign in with your Hack Club Slack account or email. Once you're logged in, install the editor plugin for your preferred code editor and start coding. <strong>Hackatime</strong> will automatically begin tracking your time.
</p>
<p class="text-lg mb-6">To start using <strong>Hackatime</strong>, simply sign in with your Hack Club Slack account or email. Once you're logged in, install the editor plugin for your preferred code editor and start coding. <strong>Hackatime</strong> will automatically begin tracking your time.</p>
<div class="text-center mt-8">
<%= link_to "Get Started with Hackatime", root_path, class: "inline-block bg-primary text-white font-bold px-8 py-3 rounded-lg hover:bg-red-600 transition-colors duration-200" %>
<%= link_to 'Get Started with Hackatime', root_path, class: 'inline-block bg-primary text-white font-bold px-8 py-3 rounded-lg hover:bg-red-600 transition-colors duration-200' %>
</div>
</div>
<div class="text-center text-gray-400 text-sm">
<p>
<strong>Hackatime</strong> is built and maintained by the Hack Club community.
<%= link_to "Learn more about Hack Club", "https://hackclub.com", target: "_blank", class: "text-primary hover:text-red underline" %>.
</p>
<p><strong>Hackatime</strong> is built and maintained by the Hack Club community. <%= link_to 'Learn more about Hack Club', 'https://hackclub.com', target: '_blank', class: 'text-primary hover:text-red underline' %>.</p>
</div>
</div>
</div>

View file

@ -1,13 +1,8 @@
<%= form_with model: user, url: (@is_own_settings ? my_settings_path : settings_user_path(user)), method: :patch, local: false, class: "space-y-4" do |f| %>
<div class="flex items-center gap-3">
<%= f.check_box :default_timezone_leaderboard,
checked: user.default_timezone_leaderboard,
id: "user_default_timezone_leaderboard",
class: "w-4 h-4 text-primary border-gray-600 rounded focus:ring-primary bg-gray-800" %>
<%= f.label :default_timezone_leaderboard, "Default to Timezone Leaderboard",
for: "user_default_timezone_leaderboard",
class: "text-sm text-gray-200" %>
<%= f.check_box :default_timezone_leaderboard, checked: user.default_timezone_leaderboard, id: 'user_default_timezone_leaderboard', class: 'w-4 h-4 text-primary border-gray-600 rounded focus:ring-primary bg-gray-800' %>
<%= f.label :default_timezone_leaderboard, 'Default to Timezone Leaderboard', for: 'user_default_timezone_leaderboard', class: 'text-sm text-gray-200' %>
</div>
<p class="text-xs text-gray-400">Access regional leaderboards that show users in your timezone region or specific timezone. Choose between timezone-specific, regional (UTC offset), or global competition modes.</p>
<%= f.submit "Save", class: "w-full px-4 py-2 bg-primary text-white font-medium rounded-lg transition-colors duration-200 cursor-pointer" %>
<%= f.submit 'Save', class: 'w-full px-4 py-2 bg-primary text-white font-medium rounded-lg transition-colors duration-200 cursor-pointer' %>
<% end %>

View file

@ -7,9 +7,8 @@ api_url = https://<%= request.host_with_port %>/api/hackatime/v1
api_key = <%= @user.api_keys.last.token %>
heartbeat_rate_limit_seconds = 30
# any other wakatime configs you want to add: https://github.com/wakatime/wakatime-cli/blob/develop/USAGE.md#ini-config-file</pre>
# any other wakatime configs you want to add: https://github.com/wakatime/wakatime-cli/blob/develop/USAGE.md#ini-config-file
</pre>
<% else %>
<p>
No API keys found. Please migrate your keys from waka.hackclub.com below. New API key generation has yet to be implemented.
</p>
<p>No API keys found. Please migrate your keys from waka.hackclub.com below. New API key generation has yet to be implemented.</p>
<% end %>

View file

@ -18,15 +18,15 @@
<%= form_with(model: [current_user, WakatimeMirror.new], local: true) do |f| %>
<div class="field">
<%= f.label :endpoint_url, "WakaTime API Endpoint" %>
<%= f.text_field :endpoint_url, value: "https://wakatime.com/api/v1", placeholder: "https://wakatime.com/api/v1" %>
<%= f.label :endpoint_url, 'WakaTime API Endpoint' %>
<%= f.text_field :endpoint_url, value: 'https://wakatime.com/api/v1', placeholder: 'https://wakatime.com/api/v1' %>
</div>
<div class="field">
<%= f.label :encrypted_api_key, "WakaTime API Key" %>
<%= f.password_field :encrypted_api_key, placeholder: "Enter your WakaTime API key" %>
<%= f.label :encrypted_api_key, 'WakaTime API Key' %>
<%= f.password_field :encrypted_api_key, placeholder: 'Enter your WakaTime API key' %>
</div>
<%= f.submit "Add Mirror", class: "button" %>
<%= f.submit 'Add Mirror', class: 'button' %>
<% end %>
</section>

View file

@ -1,11 +1,11 @@
<% content_for :title do %>
<%= @is_own_settings ? "My Settings" : "Settings | #{@user.display_name}" %>
<%= @is_own_settings ? 'My Settings' : "Settings | #{@user.display_name}" %>
<% end %>
<div class="max-w-6xl mx-auto p-6 space-y-6">
<header class="text-center mb-8">
<h1 class="text-4xl font-bold text-white mb-2">
<%= @is_own_settings ? "My Settings" : "Settings for #{@user.display_name}" %>
<%= @is_own_settings ? 'My Settings' : "Settings for #{@user.display_name}" %>
</h1>
<p class="text-muted text-lg">Change your Hackatime experience and preferences</p>
</header>
@ -19,8 +19,7 @@
<h2 class="text-xl font-semibold text-white">Time Tracking Wizard</h2>
</div>
<p class="text-gray-300 mb-4">Get started with tracking your coding time in just a few minutes.</p>
<%= link_to "Set up time tracking", my_wakatime_setup_path,
class: "inline-flex items-center gap-2 px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200" %>
<%= link_to 'Set up time tracking', my_wakatime_setup_path, class: 'inline-flex items-center gap-2 px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200' %>
</div>
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200">
@ -35,22 +34,16 @@
method: :patch, local: false,
class: "space-y-4" do |f| %>
<div>
<%= f.label :country_code, "Country flag", class: "block text-sm font-medium text-gray-200 mb-2" %>
<%= f.select :country_code,
ISO3166::Country.all.map { |c| [c.common_name, c.alpha2] }.sort_by(&:first),
{ include_blank: "Select a country" },
{ class: "w-full px-3 py-2 h-10 bg-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary" } %>
<%= f.label :country_code, 'Country flag', class: 'block text-sm font-medium text-gray-200 mb-2' %>
<%= f.select :country_code, ISO3166::Country.all.map { |c| [c.common_name, c.alpha2] }.sort_by(&:first), { include_blank: 'Select a country' }, { class: 'w-full px-3 py-2 h-10 bg-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary' } %>
</div>
<p class="text-xs text-secondary">Your country flag will be displayed on your profile and leaderboards.</p>
<div>
<%= f.label :timezone, "Timezone", class: "block text-sm font-medium text-gray-200 mb-2" %>
<%= f.select :timezone,
TZInfo::Timezone.all.map(&:identifier).sort,
{ include_blank: @user.timezone.blank? },
{ class: "w-full px-3 py-2 h-10 bg-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary" } %>
<%= f.label :timezone, 'Timezone', class: 'block text-sm font-medium text-gray-200 mb-2' %>
<%= f.select :timezone, TZInfo::Timezone.all.map(&:identifier).sort, { include_blank: @user.timezone.blank? }, { class: 'w-full px-3 py-2 h-10 bg-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary' } %>
</div>
<p class="text-xs text-secondary">This affects how your activity graph and other time-based features are displayed.</p>
<%= f.submit "Save Settings", class: "w-full px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer" %>
<%= f.submit 'Save Settings', class: 'w-full px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer' %>
<% end %>
</div>
@ -66,13 +59,10 @@
method: :patch, local: false,
class: "space-y-4" do |f| %>
<div>
<%= f.label :hackatime_extension_text_type, "Status bar text style", class: "block text-sm font-medium text-gray-200 mb-2" %>
<%= f.select :hackatime_extension_text_type,
User.hackatime_extension_text_types.keys.map { |key| [key.humanize, key] },
{},
{ class: "w-full px-3 py-2 h-10 bg-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary" } %>
<%= f.label :hackatime_extension_text_type, 'Status bar text style', class: 'block text-sm font-medium text-gray-200 mb-2' %>
<%= f.select :hackatime_extension_text_type, User.hackatime_extension_text_types.keys.map { |key| [key.humanize, key] }, {}, { class: 'w-full px-3 py-2 h-10 bg-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary' } %>
</div>
<%= f.submit "Save Settings", class: "w-full px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer" %>
<%= f.submit 'Save Settings', class: 'w-full px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer' %>
<% end %>
</div>
@ -88,24 +78,17 @@
method: :patch, local: false,
class: "space-y-4" do |f| %>
<div>
<%= f.label :username, "Custom username", class: "block text-sm font-medium text-gray-200 mb-2" %>
<%= f.text_field :username,
class: "w-full px-3 py-2 bg-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary",
placeholder: "HackClubber",
maxlength: User::USERNAME_MAX_LENGTH %>
<%= f.label :username, 'Custom username', class: 'block text-sm font-medium text-gray-200 mb-2' %>
<%= f.text_field :username, class: 'w-full px-3 py-2 bg-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary', placeholder: 'HackClubber', maxlength: User::USERNAME_MAX_LENGTH %>
<% if @user.errors[:username].present? %>
<p class="mt-1 text-xs text-red-400"><%= @user.errors[:username].to_sentence %></p>
<% end %>
</div>
<p class="text-xs text-secondary">
Choose a name to use in Hackatime. Only letters, numbers, "-" and "_" are allowed, max <%= User::USERNAME_MAX_LENGTH %> characters.
</p>
<p class="text-xs text-secondary">Choose a name to use in Hackatime. Only letters, numbers, "-" and "_" are allowed, max <%= User::USERNAME_MAX_LENGTH %> characters.</p>
<% if @user.username.present? %>
<p class="text-md text-green">
Your profile is currently live at <%= link_to "hackati.me/#{@user.username}", "https://hackati.me/#{@user.username}", target: "_blank", class: "underline" %>
</p>
<p class="text-md text-green">Your profile is currently live at <%= link_to "hackati.me/#{@user.username}", "https://hackati.me/#{@user.username}", target: '_blank', class: 'underline' %></p>
<% end %>
<%= f.submit "Save Settings", class: "w-full px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer" %>
<%= f.submit 'Save Settings', class: 'w-full px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer' %>
<% end %>
</div>
@ -122,19 +105,16 @@
<h3 class="text-lg font-medium text-white mb-2">Status Updates</h3>
<p class="text-gray-300 text-sm mb-3">When you're hacking on a project, Hackatime can update your Slack status so you can show it off!</p>
<% unless @can_enable_slack_status %>
<%= link_to "Re-authorize with Slack", slack_auth_path,
class: "inline-flex items-center gap-2 px-3 py-2 bg-darkless hover:bg-dark text-gray-200 text-sm font-medium rounded transition-colors duration-200 mb-3" %>
<%= link_to 'Re-authorize with Slack', slack_auth_path, class: 'inline-flex items-center gap-2 px-3 py-2 bg-darkless hover:bg-dark text-gray-200 text-sm font-medium rounded transition-colors duration-200 mb-3' %>
<% end %>
<%= form_with model: @user,
url: @is_own_settings ? my_settings_path : settings_user_path(@user),
method: :patch, local: false do |f| %>
<div class="flex items-center gap-3">
<%= f.check_box :uses_slack_status,
class: "w-4 h-4 text-primary border-darkless rounded focus:ring-primary bg-darkless" %>
<%= f.label :uses_slack_status, "Update my Slack status automatically",
class: "text-sm text-gray-200" %>
<%= f.check_box :uses_slack_status, class: 'w-4 h-4 text-primary border-darkless rounded focus:ring-primary bg-darkless' %>
<%= f.label :uses_slack_status, 'Update my Slack status automatically', class: 'text-sm text-gray-200' %>
</div>
<%= f.submit "Save", class: "mt-3 px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer" %>
<%= f.submit 'Save', class: 'mt-3 px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer' %>
<% end %>
</div>
@ -145,16 +125,14 @@
<ul class="space-y-1 mb-3">
<% @enabled_sailors_logs.each do |sl| %>
<li class="text-xs text-gray-300 px-2 py-1 bg-darkless rounded">
<%= render "shared/slack_channel_mention", channel_id: sl.slack_channel_id %>
<%= render 'shared/slack_channel_mention', channel_id: sl.slack_channel_id %>
</li>
<% end %>
</ul>
<% else %>
<p class="text-gray-300 text-sm mb-3">You have no notifications enabled.</p>
<% end %>
<p class="text-xs text-secondary">
You can enable notifications for specific channels by running <code class="px-1 py-0.5 bg-darkless rounded text-gray-200">/sailorslog on</code> in the Slack channel.
</p>
<p class="text-xs text-secondary">You can enable notifications for specific channels by running <code class="px-1 py-0.5 bg-darkless rounded text-gray-200">/sailorslog on</code> in the Slack channel.</p>
</div>
</div>
</div>
@ -171,13 +149,11 @@
method: :patch, local: false,
class: "space-y-4" do |f| %>
<div class="flex items-center gap-3">
<%= f.check_box :allow_public_stats_lookup,
class: "w-4 h-4 text-primary border-darkless rounded focus:ring-primary bg-darkless" %>
<%= f.label :allow_public_stats_lookup, "Allow public stats lookup",
class: "text-sm text-gray-200" %>
<%= f.check_box :allow_public_stats_lookup, class: 'w-4 h-4 text-primary border-darkless rounded focus:ring-primary bg-darkless' %>
<%= f.label :allow_public_stats_lookup, 'Allow public stats lookup', class: 'text-sm text-gray-200' %>
</div>
<p class="text-xs text-secondary">When enabled, others can view your coding statistics through public APIs. Many Hack Club YSWS programs use this to track your progress. Disabling this can prevent you from participating in some programs.</p>
<%= f.submit "Save Settings", class: "w-full px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer" %>
<%= f.submit 'Save Settings', class: 'w-full px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer' %>
<% end %>
<div class="border-t border-darkless pt-4 mt-4 space-y-3">
@ -189,19 +165,10 @@
</div>
<% if @user.can_request_deletion? %>
<p class="text-gray-300 text-sm">
Permanently delete your account and all associated data. This action cannot be undone after the 30-day grace period.
</p>
<button type="button"
data-controller="account-deletion"
data-action="click->account-deletion#confirm"
class="w-full px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer">
Request Account Deletion
</button>
<p class="text-gray-300 text-sm">Permanently delete your account and all associated data. This action cannot be undone after the 30-day grace period.</p>
<button type="button" data-controller="account-deletion" data-action="click->account-deletion#confirm" class="w-full px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer">Request Account Deletion</button>
<% else %>
<p class="text-white text-sm">
Due to your account standing, you cannot request account deletion at this time. Reach out in #hackatime-v2 if this is a mistake.
</p>
<p class="text-white text-sm">Due to your account standing, you cannot request account deletion at this time. Reach out in #hackatime-v2 if this is a mistake.</p>
<% end %>
</div>
</div>
@ -215,16 +182,9 @@
</div>
<div class="space-y-4">
<p class="text-gray-300 text-sm">
Your API key is used to authenticate requests from your code editor. If your key has been compromised, you can rotate it to generate a new one. Rotating your API key will immediately invalidate your old key. You'll need to update the key in all of your code editors and IDEs.
</p>
<p class="text-gray-300 text-sm">Your API key is used to authenticate requests from your code editor. If your key has been compromised, you can rotate it to generate a new one. Rotating your API key will immediately invalidate your old key. You'll need to update the key in all of your code editors and IDEs.</p>
<button type="button"
data-controller="api-key-rotation"
data-action="click->api-key-rotation#confirm"
class="w-full px-4 py-2 bg-primary hover:bg-primary/75 text-white font-medium rounded transition-colors duration-200 cursor-pointer">
Rotate API Key
</button>
<button type="button" data-controller="api-key-rotation" data-action="click->api-key-rotation#confirm" class="w-full px-4 py-2 bg-primary hover:bg-primary/75 text-white font-medium rounded transition-colors duration-200 cursor-pointer">Rotate API Key</button>
</div>
</div>
@ -243,15 +203,14 @@
<% if @user.github_uid.present? %>
<div class="flex items-center gap-2 p-3 bg-darkless border border-darkless rounded">
<span class="text-green-400">✅</span>
<span class="text-gray-200 text-sm">Connected: <%= link_to "@#{h(@user.github_username)}", "https://github.com/#{h(@user.github_username)}", target: "_blank", class: "text-primary hover:text-primary/80 underline" %></span>
<span class="text-gray-200 text-sm">Connected: <%= link_to "@#{h(@user.github_username)}", "https://github.com/#{h(@user.github_username)}", target: '_blank', class: 'text-primary hover:text-primary/80 underline' %></span>
</div>
<div class="flex items-center gap-2">
<%= link_to "Relink GitHub Account", github_auth_path, data: { turbo: "false" }, class: "inline-flex items-center gap-2 px-3 py-2 bg-primary text-white hover:bg-primary/75 text-sm font-medium rounded transition-colors duration-200 cursor-pointer" %>
<%= button_to "Unlink", github_unlink_path, method: :delete, data: { turbo_confirm: "Are you sure you want to unlink your GitHub account? This will remove your GitHub connection and you may need to re-link to use GitHub-dependent features." }, class: "inline-flex items-center gap-2 px-3 py-2 bg-darkless hover:bg-darkless/50 text-white hover:text-white/80 text-sm font-medium rounded transition-colors duration-200 cursor-pointer" %>
<%= link_to 'Relink GitHub Account', github_auth_path, data: { turbo: 'false' }, class: 'inline-flex items-center gap-2 px-3 py-2 bg-primary text-white hover:bg-primary/75 text-sm font-medium rounded transition-colors duration-200 cursor-pointer' %>
<%= button_to 'Unlink', github_unlink_path, method: :delete, data: { turbo_confirm: 'Are you sure you want to unlink your GitHub account? This will remove your GitHub connection and you may need to re-link to use GitHub-dependent features.' }, class: 'inline-flex items-center gap-2 px-3 py-2 bg-darkless hover:bg-darkless/50 text-white hover:text-white/80 text-sm font-medium rounded transition-colors duration-200 cursor-pointer' %>
</div>
<% else %>
<%= link_to "Link GitHub Account", github_auth_path, data: { turbo: "false" },
class: "inline-flex items-center gap-2 px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer" %>
<%= link_to 'Link GitHub Account', github_auth_path, data: { turbo: 'false' }, class: 'inline-flex items-center gap-2 px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer' %>
<% end %>
</div>
@ -265,7 +224,7 @@
<div class="flex items-center gap-2 p-2 bg-darkless border border-darkless rounded grow">
<span class="text-gray-300 text-sm"><%= email.email %></span>
<span class="text-xs px-2 py-1 bg-dark text-secondary rounded">
<%= email.source&.humanize || "Unknown" %>
<%= email.source&.humanize || 'Unknown' %>
</span>
</div>
<% if @user.can_delete_email_address?(email) %>
@ -273,7 +232,7 @@
method: :delete,
class: "space-y-4" do |f| %>
<%= f.hidden_field :email, value: email.email %>
<%= f.submit "Unlink!", class: "w-full px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer" %>
<%= f.submit 'Unlink!', class: 'w-full px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer' %>
<% end %>
<% end %>
</div>
@ -283,11 +242,8 @@
<p class="text-secondary text-sm">No email addresses found.</p>
<% end %>
<%= form_tag add_email_auth_path, data: { turbo: false }, class: "space-y-2" do %>
<%= email_field_tag :email, nil,
placeholder: "Add another email address",
required: true,
class: "w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary text-sm" %>
<%= submit_tag "Add Email", class: "w-full px-3 py-2 bg-primary hover:bg-primary/75 text-white text-sm font-medium rounded transition-colors duration-200 cursor-pointer" %>
<%= email_field_tag :email, nil, placeholder: 'Add another email address', required: true, class: 'w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary text-sm' %>
<%= submit_tag 'Add Email', class: 'w-full px-3 py-2 bg-primary hover:bg-primary/75 text-white text-sm font-medium rounded transition-colors duration-200 cursor-pointer' %>
<% end %>
</div>
</div>
@ -309,15 +265,13 @@
<div class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-200 mb-2">Theme</label>
<select name="theme" id="theme-select" onchange="up1(this.value)"
class="w-full px-3 py-2 h-10 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary">
<% GithubReadmeStats.themes.each do |theme| %>
<option value="<%= theme %>"><%= theme.humanize %></option>
<select name="theme" id="theme-select" onchange="up1(this.value)" class="w-full px-3 py-2 h-10 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary">
<% GithubReadmeStats.themes.each do |theme| %><option value="<%= theme %>"><%= theme.humanize %></option>
<% end %>
</select>
</div>
<% gh_badge = GithubReadmeStats.new(current_user.id, "darcula") %>
<% gh_badge = GithubReadmeStats.new(current_user.id, 'darcula') %>
<div class="p-4 bg-darkless border border-darkless rounded">
<img id="badge-preview" src="<%= gh_badge.generate_badge_url %>" data-url="<%= gh_badge.generate_badge_url %>" class="mb-3 rounded">
<pre id="badge-url" class="text-xs text-white bg-darker p-2 rounded overflow-x-auto"><%= gh_badge.generate_badge_url %></pre>
@ -330,10 +284,8 @@
<h3 class="text-lg font-medium text-white mb-2">Project Stats Badge</h3>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-200">Project</label>
<select name="project" id="project-select" onchange="up2(this.value)"
class="w-full px-3 py-2 h-10 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary">
<% @projects.each do |project| %>
<option value="<%= h(project) %>"><%= h(project) %></option>
<select name="project" id="project-select" onchange="up2(this.value)" class="w-full px-3 py-2 h-10 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary">
<% @projects.each do |project| %><option value="<%= h(project) %>"><%= h(project) %></option>
<% end %>
</select>
<div class="mt-3 p-4 bg-darkless border border-darkless rounded">
@ -347,18 +299,18 @@
<script>
function up1(theme) {
const preview = document.getElementById('badge-preview');
const url = document.getElementById('badge-url');
const baseUrl = preview.dataset.url.replace(/theme=[^&]*/, '');
const newUrl = baseUrl + (baseUrl.includes('?') ? '&' : '?') + 'theme=' + theme;
const preview = document.getElementById("badge-preview");
const url = document.getElementById("badge-url");
const baseUrl = preview.dataset.url.replace(/theme=[^&]*/, "");
const newUrl = baseUrl + (baseUrl.includes("?") ? "&" : "?") + "theme=" + theme;
preview.src = newUrl;
url.textContent = newUrl;
}
function up2(project) {
const preview = document.getElementById('project-badge-preview');
const url = document.getElementById('project-badge-url');
const baseUrl = <%== (@work_time_stats_url.gsub(@projects.first || 'example', '')).to_json %>;
const preview = document.getElementById("project-badge-preview");
const url = document.getElementById("project-badge-url");
const baseUrl = <%= = (@work_time_stats_url.gsub(@projects.first || 'example', '')).to_json %>;
const newUrl = baseUrl + project;
preview.src = newUrl;
url.textContent = newUrl;
@ -377,11 +329,9 @@
<p class="text-gray-300 text-sm mb-4">Your Wakatime configuration file for tracking coding time.</p>
<div class="bg-darkless border border-darkless rounded p-4 overflow-x-auto">
<%= render "wakatime_config_display" %>
<%= render 'wakatime_config_display' %>
</div>
<p class="text-xs text-secondary mt-2">
This configuration file is automatically generated and updated when you make changes to your settings.
</p>
<p class="text-xs text-secondary mt-2">This configuration file is automatically generated and updated when you make changes to your settings.</p>
</div>
<div class="border-t border-darkless pt-6">
@ -393,16 +343,13 @@
</div>
<p class="text-gray-300 text-sm mb-4">This will migrate your heartbeats from waka.hackclub.com to this platform.</p>
<%= button_to "Migrate heartbeats", my_settings_migrate_heartbeats_path, method: :post,
class: "w-full px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer" %>
<%= button_to 'Migrate heartbeats', my_settings_migrate_heartbeats_path, method: :post, class: 'w-full px-4 py-2 bg-primary text-white hover:bg-primary/75 font-medium rounded transition-colors duration-200 cursor-pointer' %>
<% if @heartbeats_migration_jobs.any? %>
<div class="mt-4 space-y-2">
<h3 class="text-sm font-medium text-white">Migration Status</h3>
<% @heartbeats_migration_jobs.each do |job| %>
<div class="p-2 bg-darkless border border-darkless rounded text-xs text-gray-300">
Job ID: <%= job.id %> - Status: <%= job.status %>
</div>
<div class="p-2 bg-darkless border border-darkless rounded text-xs text-gray-300">Job ID: <%= job.id %> - Status: <%= job.status %></div>
<% end %>
</div>
<% end %>
@ -427,9 +374,7 @@
<p class="text-xs text-secondary">See the <a href="https://github.com/taciturnaxolotl/markscribe#your-wakatime-languages-formated-as-a-bar" target="_blank" class="text-primary hover:text-primary/80 underline">markscribe documentation</a> for more template options.</p>
</div>
<div>
<img src="https://cdn.fluff.pw/slackcdn/524e293aa09bc5f9115c0c29c18fb4bc.png"
alt="Example of markscribe output showing coding language and project statistics"
class="w-full rounded border border-darkless">
<img src="https://cdn.fluff.pw/slackcdn/524e293aa09bc5f9115c0c29c18fb4bc.png" alt="Example of markscribe output showing coding language and project statistics" class="w-full rounded border border-darkless">
</div>
</div>
</div>
@ -438,122 +383,112 @@
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200 md:col-span-2">
<% if @user.trust_level == "red" %>
<div class="flex items-center gap-3 mb-4">
<div class="p-2 bg-primary/10 rounded">
<span class="text-2xl">💾</span>
<div class="flex items-center gap-3 mb-4">
<div class="p-2 bg-primary/10 rounded">
<span class="text-2xl">💾</span>
</div>
<h2 class="text-xl font-semibold text-white" id="download_user_data">Download Your Data</h2>
</div>
<h2 class="text-xl font-semibold text-white" id="download_user_data">Download Your Data</h2>
</div>
<div class="bg-red-500/20 border border-red-500 rounded-lg p-4">
<div class="flex items-center gap-2">
<span class="text-red-500 font-medium">⚠️ Export Restricted</span>
</div>
<p class="text-red-500 text-sm mt-2">
Sorry, due to your account standing, you are unable to perform this action.
</p>
<p class="text-red-500 text-sm mt-2">Sorry, due to your account standing, you are unable to perform this action.</p>
</div>
<% else %>
<div class="flex items-center gap-3 mb-4">
<div class="p-2 bg-primary/10 rounded">
<span class="text-2xl">💾</span>
<div class="flex items-center gap-3 mb-4">
<div class="p-2 bg-primary/10 rounded">
<span class="text-2xl">💾</span>
</div>
<h2 class="text-xl font-semibold text-white" id="download_user_data">Download Your Data</h2>
</div>
<h2 class="text-xl font-semibold text-white" id="download_user_data">Download Your Data</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-3">
<h3 class="text-lg font-medium text-white">Your Data Overview</h3>
<div class="grid grid-cols-1 gap-4">
<div class="bg-darkless border border-darkless rounded p-4">
<div class="text-center">
<div class="text-2xl font-bold text-primary mb-1"><%= number_with_delimiter(@user.heartbeats.count) %></div>
<div class="text-sm text-gray-300">Total Heartbeats</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-3">
<h3 class="text-lg font-medium text-white">Your Data Overview</h3>
<div class="grid grid-cols-1 gap-4">
<div class="bg-darkless border border-darkless rounded p-4">
<div class="text-center">
<div class="text-2xl font-bold text-primary mb-1"><%= number_with_delimiter(@user.heartbeats.count) %></div>
<div class="text-sm text-gray-300">Total Heartbeats</div>
</div>
</div>
</div>
<div class="bg-darkless border border-darkless rounded p-4">
<div class="text-center">
<div class="text-2xl font-bold text-orange mb-1"><%= @user.heartbeats.duration_simple %></div>
<div class="text-sm text-gray-300">Total Coding Time</div>
<div class="bg-darkless border border-darkless rounded p-4">
<div class="text-center">
<div class="text-2xl font-bold text-orange mb-1"><%= @user.heartbeats.duration_simple %></div>
<div class="text-sm text-gray-300">Total Coding Time</div>
</div>
</div>
</div>
<div class="bg-darkless border border-darkless rounded p-4">
<div class="text-center">
<div class="text-2xl font-bold text-primary mb-1"><%= @user.heartbeats.where("time >= ?", 7.days.ago.to_f).count %></div>
<div class="text-sm text-gray-300">Heartbeats in the Last 7 Days</div>
<div class="bg-darkless border border-darkless rounded p-4">
<div class="text-center">
<div class="text-2xl font-bold text-primary mb-1"><%= @user.heartbeats.where('time >= ?', 7.days.ago.to_f).count %></div>
<div class="text-sm text-gray-300">Heartbeats in the Last 7 Days</div>
</div>
</div>
</div>
</div>
</div>
<div class="space-y-4">
<h3 class="text-lg font-medium text-white">Export Options</h3>
<div class="space-y-4">
<h3 class="text-lg font-medium text-white">Export Options</h3>
<div class="bg-darkless border border-darkless rounded p-4">
<div class="flex items-center gap-2 mb-3">
<svg class="w-5 h-5 text-primary" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
</svg>
<h4 class="text-white font-medium">Heartbeat Data</h4>
</div>
<p class="text-gray-300 text-sm mb-3">Export your coding activity as JSON with detailed information about each coding session.</p>
<div class="space-y-2">
<%= link_to export_my_heartbeats_path(format: :json, all_data: "true"),
class: "w-full bg-primary hover:bg-primary/75 text-white px-4 py-2 rounded font-medium transition-colors inline-flex items-center justify-center gap-2",
method: :get do %>
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<div class="bg-darkless border border-darkless rounded p-4">
<div class="flex items-center gap-2 mb-3">
<svg class="w-5 h-5 text-primary" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
</svg>
Export All Heartbeats
<% end %>
<button type="button"
class="w-full bg-dark hover:bg-dark/75 border border-darkless text-white px-4 py-2 rounded font-medium transition-colors inline-flex items-center justify-center gap-2 cursor-pointer"
data-controller="heartbeat-export"
data-action="click->heartbeat-export#openModal">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Export Date Range
</button>
</div>
<div class="mt-3 text-xs text-secondary">
<p><strong>All Heartbeats:</strong> Downloads your complete coding history, from the very start to your last heartbeat</p>
<p><strong>Date Range:</strong> Choose specific dates to export</p>
</div>
</div>
<% dev_tool do %>
<div class="p-6 bg-darkless border border-darkless rounded">
<div class="flex items-center gap-3 mb-3">
<div class="p-2 bg-green-600/10 rounded">
<svg class="w-4 h-4 text-green-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
</div>
<h4 class="text-white font-medium">Import Heartbeat Data</h4>
<h4 class="text-white font-medium">Heartbeat Data</h4>
</div>
<p class="text-gray-300 text-sm mb-4">Import ur data from real hackatime to test stuff with.</p>
<p class="text-gray-300 text-sm mb-4">PS: your console will be spammed and might crash ur dev env so be careful if the file is very big</p>
<%= form_with url: import_my_heartbeats_path, method: :post, multipart: true, local: true, class: "space-y-4" do |form| %>
<div>
<%= form.file_field :heartbeat_file,
accept: ".json,application/json",
class: "w-full px-3 py-2 bg-dark border border-darkless rounded text-white file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-primary file:text-white hover:file:bg-primary/75 transition-colors cursor-pointer",
required: true %>
</div>
<p class="text-gray-300 text-sm mb-3">Export your coding activity as JSON with detailed information about each coding session.</p>
<div class="flex gap-3">
<%= form.submit "Import Heartbeats",
class: "w-full px-3 py-2 bg-primary hover:bg-primary/75 text-white text-md font-medium rounded transition-colors duration-200 cursor-pointer",
data: { confirm: "Are you sure you want to import heartbeats? This will add new data to your account." } %>
</div>
<% end %>
<div class="space-y-2">
<%= link_to export_my_heartbeats_path(format: :json, all_data: "true"),
class: "w-full bg-primary hover:bg-primary/75 text-white px-4 py-2 rounded font-medium transition-colors inline-flex items-center justify-center gap-2",
method: :get do %>
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
</svg>
Export All Heartbeats
<% end %>
<button type="button" class="w-full bg-dark hover:bg-dark/75 border border-darkless text-white px-4 py-2 rounded font-medium transition-colors inline-flex items-center justify-center gap-2 cursor-pointer" data-controller="heartbeat-export" data-action="click->heartbeat-export#openModal">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Export Date Range
</button>
</div>
<div class="mt-3 text-xs text-secondary">
<p><strong>All Heartbeats:</strong> Downloads your complete coding history, from the very start to your last heartbeat</p>
<p><strong>Date Range:</strong> Choose specific dates to export</p>
</div>
</div>
<% end %>
<% dev_tool do %>
<div class="p-6 bg-darkless border border-darkless rounded">
<div class="flex items-center gap-3 mb-3">
<div class="p-2 bg-green-600/10 rounded">
<svg class="w-4 h-4 text-green-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
</div>
<h4 class="text-white font-medium">Import Heartbeat Data</h4>
</div>
<p class="text-gray-300 text-sm mb-4">Import ur data from real hackatime to test stuff with.</p>
<p class="text-gray-300 text-sm mb-4">PS: your console will be spammed and might crash ur dev env so be careful if the file is very big</p>
<%= form_with url: import_my_heartbeats_path, method: :post, multipart: true, local: true, class: "space-y-4" do |form| %>
<div>
<%= form.file_field :heartbeat_file, accept: '.json,application/json', class: 'w-full px-3 py-2 bg-dark border border-darkless rounded text-white file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-primary file:text-white hover:file:bg-primary/75 transition-colors cursor-pointer', required: true %>
</div>
<div class="flex gap-3">
<%= form.submit 'Import Heartbeats', class: 'w-full px-3 py-2 bg-primary hover:bg-primary/75 text-white text-md font-medium rounded transition-colors duration-200 cursor-pointer', data: { confirm: 'Are you sure you want to import heartbeats? This will add new data to your account.' } %>
</div>
<% end %>
</div>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
@ -567,14 +502,12 @@
</div>
<% if current_user.wakatime_mirrors.any? %>
<% grid_cols = current_user.wakatime_mirrors.size > 1 ? "md:grid-cols-2" : "" %>
<% grid_cols = current_user.wakatime_mirrors.size > 1 ? 'md:grid-cols-2' : '' %>
<div class="grid grid-cols-1 <%= grid_cols %> gap-4 mb-4">
<% current_user.wakatime_mirrors.each do |mirror| %>
<div class="p-4 bg-darkless border border-darkless rounded">
<h3 class="text-white font-medium"><%= mirror.endpoint_url %></h3>
<p class="text-secondary text-sm">
Last synced: <%= mirror.last_synced_at ? time_ago_in_words(mirror.last_synced_at) + " ago" : "Never" %>
</p>
<p class="text-secondary text-sm">Last synced: <%= mirror.last_synced_at ? time_ago_in_words(mirror.last_synced_at) + ' ago' : 'Never' %></p>
</div>
<% end %>
</div>
@ -583,15 +516,15 @@
<%= form_with(model: [current_user, WakatimeMirror.new], local: true, class: "space-y-4") do |f| %>
<div class="grid grid-cols-1 gap-4">
<div>
<%= f.label :endpoint_url, class: "block text-sm font-medium text-gray-200 mb-2" %>
<%= f.url_field :endpoint_url, value: "https://wakatime.com/api/v1", class: "w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary" %>
<%= f.label :endpoint_url, class: 'block text-sm font-medium text-gray-200 mb-2' %>
<%= f.url_field :endpoint_url, value: 'https://wakatime.com/api/v1', class: 'w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary' %>
</div>
<div>
<%= f.label :encrypted_api_key, "WakaTime API Key", class: "block text-sm font-medium text-gray-200 mb-2" %>
<%= f.password_field :encrypted_api_key, placeholder: "Enter your WakaTime API key", class: "w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary" %>
<%= f.label :encrypted_api_key, 'WakaTime API Key', class: 'block text-sm font-medium text-gray-200 mb-2' %>
<%= f.password_field :encrypted_api_key, placeholder: 'Enter your WakaTime API key', class: 'w-full px-3 py-2 bg-darkless border border-darkless rounded text-white focus:border-primary focus:ring-1 focus:ring-primary' %>
</div>
</div>
<%= f.submit "Add Mirror", class: "px-4 py-2 bg-primary text-white font-medium rounded transition-colors duration-200" %>
<%= f.submit 'Add Mirror', class: 'px-4 py-2 bg-primary text-white font-medium rounded transition-colors duration-200' %>
<% end %>
</div>
<% end %>
@ -599,80 +532,27 @@
</div>
<div data-controller="api-key-rotation">
<%= render "shared/modal",
modal_id: "api-key-confirm-modal",
title: "Rotate API Key?",
description: "Your old key will be immediately invalidated and you'll need to update it in all your applications.",
icon_svg: '<path fill="currentColor" d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/>',
icon_color: "text-primary",
buttons: [
{
text: "Cancel",
class: "bg-dark hover:bg-darkless border border-darkless text-gray-300",
action: "click->modal#close"
},
{
text: "Rotate Now",
class: "bg-primary hover:bg-primary/75 text-white font-medium",
action: "click->api-key-rotation#rotate"
}
] %>
<%= render 'shared/modal', modal_id: 'api-key-confirm-modal', title: 'Rotate API Key?', description: "Your old key will be immediately invalidated and you'll need to update it in all your applications.", icon_svg: '<path fill="currentColor" d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/>', icon_color: 'text-primary', buttons: [{ text: 'Cancel', class: 'bg-dark hover:bg-darkless border border-darkless text-gray-300', action: 'click->modal#close' }, { text: 'Rotate Now', class: 'bg-primary hover:bg-primary/75 text-white font-medium', action: 'click->api-key-rotation#rotate' }] %>
<%= render "shared/modal",
modal_id: "api-key-success-modal",
title: "New API Key Generated",
description: "Your old API key has been invalidated. Update your editor configuration with this new key:",
icon_svg: '<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>',
icon_color: "text-green-500",
max_width: "max-w-lg",
buttons: [
{
text: "Close",
class: "bg-dark hover:bg-darkless border border-darkless text-gray-300",
action: "click->modal#close"
},
{
text: "Copy Key",
class: "bg-primary hover:bg-primary/75 text-white font-medium",
action: "click->api-key-rotation#copyKey"
}
],
custom: '<div class="w-full mb-4"><div class="bg-darker rounded-lg p-3"><code id="new-api-key-display" class="text-sm text-white break-all" data-token=""></code></div></div>' %>
<%= render 'shared/modal', modal_id: 'api-key-success-modal', title: 'New API Key Generated', description: 'Your old API key has been invalidated. Update your editor configuration with this new key:', icon_svg: '<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>', icon_color: 'text-green-500', max_width: 'max-w-lg', buttons: [{ text: 'Close', class: 'bg-dark hover:bg-darkless border border-darkless text-gray-300', action: 'click->modal#close' }, { text: 'Copy Key', class: 'bg-primary hover:bg-primary/75 text-white font-medium', action: 'click->api-key-rotation#copyKey' }], custom: '<div class="w-full mb-4"><div class="bg-darker rounded-lg p-3"><code id="new-api-key-display" class="text-sm text-white break-all" data-token=""></code></div></div>' %>
</div>
<% if @user.can_request_deletion? %>
<div data-controller="account-deletion">
<%= render "shared/modal",
modal_id: "account-deletion-confirm-modal",
title: "Delete Your Account?",
description: "This will permanently delete your account after a 30 day waiting period. During this time, you won't be able to use your account for any Hack Club programs.",
icon_svg: '<path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>',
icon_color: "text-primary",
buttons: [
{
text: "Cancel",
class: "bg-dark hover:bg-darkless border border-darkless text-gray-300",
action: "click->modal#close"
},
{
text: "Delete My Account",
class: "bg-primary hover:bg-primary/75 text-white font-medium",
form: true,
url: create_deletion_path,
method: "post"
}
] %>
</div>
<div data-controller="account-deletion">
<%= render 'shared/modal', modal_id: 'account-deletion-confirm-modal', title: 'Delete Your Account?', description: "This will permanently delete your account after a 30 day waiting period. During this time, you won't be able to use your account for any Hack Club programs.", icon_svg: '<path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>', icon_color: 'text-primary', buttons: [{ text: 'Cancel', class: 'bg-dark hover:bg-darkless border border-darkless text-gray-300', action: 'click->modal#close' }, { text: 'Delete My Account', class: 'bg-primary hover:bg-primary/75 text-white font-medium', form: true, url: create_deletion_path, method: 'post' }] %>
</div>
<% end %>
<%= render "shared/modal",
modal_id: "export-date-range-modal",
title: "Export Date Range",
description: "Choose specific dates to export your coding activity.",
icon_svg: '<path fill="currentColor" d="M19 4h-1V3c0-.55-.45-1-1-1s-1 .45-1 1v1H8V3c0-.55-.45-1-1-1s-1 .45-1 1v1H5c-1.11 0-1.99.9-1.99 2L3 20a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2m0 15c0 .55-.45 1-1 1H6c-.55 0-1-.45-1-1V9h14zM7 11h2v2H7zm4 0h2v2h-2zm4 0h2v2h-2z"/>',
icon_color: "text-primary",
max_width: "max-w-lg",
custom: '
<%=
render 'shared/modal',
modal_id: 'export-date-range-modal',
title: 'Export Date Range',
description: 'Choose specific dates to export your coding activity.',
icon_svg: '<path fill="currentColor" d="M19 4h-1V3c0-.55-.45-1-1-1s-1 .45-1 1v1H8V3c0-.55-.45-1-1-1s-1 .45-1 1v1H5c-1.11 0-1.99.9-1.99 2L3 20a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2m0 15c0 .55-.45 1-1 1H6c-.55 0-1-.45-1-1V9h14zM7 11h2v2H7zm4 0h2v2h-2zm4 0h2v2h-2z"/>',
icon_color: 'text-primary',
max_width: 'max-w-lg',
custom:
'
<form id="export-date-range-form" class="w-full space-y-4">
<div class="space-y-2">
<label for="export-start-date" class="block text-sm font-semibold text-white">Start Date</label>
@ -685,16 +565,5 @@
class="w-full px-4 py-3 bg-darkless text-white border border-darkless rounded-lg focus:border-primary focus:outline-none transition-colors">
</div>
</form>',
buttons: [
{
text: "Cancel",
class: "bg-dark hover:bg-darkless border border-darkless text-gray-300",
action: "click->modal#close"
},
{
text: "Export",
class: "bg-primary hover:bg-primary/75 text-white font-medium",
form: true,
form_id: "export-date-range-form"
}
] %>
buttons: [{ text: 'Cancel', class: 'bg-dark hover:bg-darkless border border-darkless text-gray-300', action: 'click->modal#close' }, { text: 'Export', class: 'bg-primary hover:bg-primary/75 text-white font-medium', form: true, form_id: 'export-date-range-form' }]
%>

View file

@ -166,9 +166,7 @@
</div>
</section>
<p class="text-center text-secondary text-xs mt-3">
Already configured? <a href="<%= my_wakatime_setup_step_2_path %>" class="text-cyan hover:underline">Skip to next step</a>
</p>
<p class="text-center text-secondary text-xs mt-3">Already configured? <a href="<%= my_wakatime_setup_step_2_path %>" class="text-cyan hover:underline">Skip to next step</a></p>
</div>
</div>
</div>
@ -178,109 +176,103 @@
<script>
function a() {
const ua = window.navigator.userAgent;
const mac = document.getElementById('mac-linux');
const windows = document.getElementById('windows');
const mac = document.getElementById("mac-linux");
const windows = document.getElementById("windows");
mac.style.display = 'none';
windows.style.display = 'none';
mac.style.display = "none";
windows.style.display = "none";
if (ua.indexOf('Windows') !== -1) {
windows.style.display = 'block';
if (ua.indexOf("Windows") !== -1) {
windows.style.display = "block";
} else {
mac.style.display = 'block';
mac.style.display = "block";
}
}
document.addEventListener('turbo:load', function() {
document.addEventListener("turbo:load", function () {
a();
window.toggleSection = function(section) {
const mac = document.getElementById('mac-linux');
const windows = document.getElementById('windows');
const advanced = document.getElementById('advanced');
window.toggleSection = function (section) {
const mac = document.getElementById("mac-linux");
const windows = document.getElementById("windows");
const advanced = document.getElementById("advanced");
mac.style.display = 'none';
windows.style.display = 'none';
advanced.style.display = 'none';
mac.style.display = "none";
windows.style.display = "none";
advanced.style.display = "none";
if (section === 'windows') {
windows.style.display = 'block';
} else if (section === 'advanced') {
advanced.style.display = 'block';
if (section === "windows") {
windows.style.display = "block";
} else if (section === "advanced") {
advanced.style.display = "block";
} else {
mac.style.display = 'block';
mac.style.display = "block";
}
}
};
const waitingState = document.getElementById('waiting-state');
const successState = document.getElementById('success-state');
const statusMessage = document.getElementById('status-message');
const pollStatus = document.getElementById('poll-status');
const statusPanel = document.getElementById('status-panel');
const waitingState = document.getElementById("waiting-state");
const successState = document.getElementById("success-state");
const statusMessage = document.getElementById("status-message");
const pollStatus = document.getElementById("poll-status");
const statusPanel = document.getElementById("status-panel");
let checkCount = 0;
const maxChecks = 120;
const msg = [
"Copy the command on the left and run it in your terminal!",
"Paste the command and press Enter...",
"The script will configure everything automatically!",
"Almost there - just run the command!",
"We'll detect it as soon as the script runs!"
];
const msg = ["Copy the command on the left and run it in your terminal!", "Paste the command and press Enter...", "The script will configure everything automatically!", "Almost there - just run the command!", "We'll detect it as soon as the script runs!"];
function showSuccess(timeAgo) {
waitingState.classList.add('hidden');
successState.classList.remove('hidden');
statusPanel.classList.remove('border-darkless');
statusPanel.classList.add('border-green');
document.getElementById('heartbeat-time-ago').textContent = timeAgo;
waitingState.classList.add("hidden");
successState.classList.remove("hidden");
statusPanel.classList.remove("border-darkless");
statusPanel.classList.add("border-green");
document.getElementById("heartbeat-time-ago").textContent = timeAgo;
}
function check() {
fetch(<%== api_v1_my_heartbeats_most_recent_path(source_type: "test_entry").to_json %>, {
fetch(<%= = api_v1_my_heartbeats_most_recent_path(source_type: "test_entry").to_json %>, {
headers: {
'Authorization': 'Bearer ' + <%== @current_user_api_key.to_json %>
}
Authorization: "Bearer " + <%= = @current_user_api_key.to_json %>,
},
})
.then(response => response.json())
.then(data => {
if (data.has_heartbeat) {
const heartbeatTime = new Date(data.heartbeat.created_at);
const now = new Date();
const secondsAgo = (now - heartbeatTime) / 1000;
const recentThreshold = 300;
.then((response) => response.json())
.then((data) => {
if (data.has_heartbeat) {
const heartbeatTime = new Date(data.heartbeat.created_at);
const now = new Date();
const secondsAgo = (now - heartbeatTime) / 1000;
const recentThreshold = 300;
if (secondsAgo <= recentThreshold) {
showSuccess(data.time_ago);
if (secondsAgo <= recentThreshold) {
showSuccess(data.time_ago);
return;
}
}
throw new Error("No heartbeats yet");
})
.catch((error) => {
checkCount++;
if (checkCount % 3 === 0) {
const msgIndex = Math.floor(checkCount / 3) % msg.length;
statusMessage.textContent = msg[msgIndex];
}
pollStatus.textContent = `Checked ${checkCount} time${checkCount === 1 ? "" : "s"}...`;
if (checkCount >= maxChecks) {
pollStatus.textContent = "Still waiting... Make sure you've run the command!";
return;
}
}
throw new Error('No heartbeats yet');
})
.catch(error => {
checkCount++;
if (checkCount % 3 === 0) {
const msgIndex = Math.floor(checkCount / 3) % msg.length;
statusMessage.textContent = msg[msgIndex];
}
pollStatus.textContent = `Checked ${checkCount} time${checkCount === 1 ? '' : 's'}...`;
if (checkCount >= maxChecks) {
pollStatus.textContent = "Still waiting... Make sure you've run the command!";
return;
}
setTimeout(check, 5000);
});
setTimeout(check, 5000);
});
}
check();
window.skipToNext = function() {
window.location.href = <%== my_wakatime_setup_step_2_path.to_json %>;
window.skipToNext = function () {
window.location.href = <%= = my_wakatime_setup_step_2_path.to_json %>;
};
});
@ -289,13 +281,13 @@
const text = codeBlock.textContent;
navigator.clipboard.writeText(text).then(() => {
const originalText = button.textContent;
button.textContent = '✅ Copied!';
button.classList.add('bg-green');
button.classList.remove('bg-primary');
button.textContent = "✅ Copied!";
button.classList.add("bg-green");
button.classList.remove("bg-primary");
setTimeout(() => {
button.textContent = originalText;
button.classList.remove('bg-green');
button.classList.add('bg-primary');
button.classList.remove("bg-green");
button.classList.add("bg-primary");
}, 2000);
});
}
@ -312,8 +304,12 @@
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.setup-instructions {
@ -332,7 +328,7 @@
min-width: 0;
white-space: pre-wrap;
word-break: break-all;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
line-height: 1.4;
}

View file

@ -27,9 +27,7 @@
<section class="bg-dark rounded-lg p-6">
<h3 class="text-2xl font-bold text-blue mb-4">💻 Install the VS Code Extension</h3>
<div class="space-y-4">
<p class="text-lg">
Install the <a href="https://marketplace.visualstudio.com/items?itemName=WakaTime.vscode-wakatime" target="_blank" rel="noopener noreferrer" class="text-cyan hover:text-blue underline font-semibold">WakaTime extension from the marketplace</a>.
</p>
<p class="text-lg">Install the <a href="https://marketplace.visualstudio.com/items?itemName=WakaTime.vscode-wakatime" target="_blank" rel="noopener noreferrer" class="text-cyan hover:text-blue underline font-semibold">WakaTime extension from the marketplace</a>.</p>
<h4 class="font-bold mb-2">Step-by-step:</h4>
<ol class="list-decimal list-inside space-y-1">
@ -109,20 +107,15 @@
</div>
</section>
<p class="text-center text-secondary text-xs mt-3">
Already set up? <a href="<%= my_wakatime_setup_step_4_path %>" class="text-cyan hover:underline">Skip to finish</a>
</p>
<p class="text-center text-secondary text-xs mt-3">Already set up? <a href="<%= my_wakatime_setup_step_4_path %>" class="text-cyan hover:underline">Skip to finish</a></p>
</div>
</div>
</div>
<% elsif params[:editor] == "vim" %>
<section id="vim" class="bg-dark rounded-lg p-6 mb-6">
<h3 class="text-2xl font-bold text-green mb-4">📟 Vim</h3>
<div class="space-y-6">
<p class="text-lg">
Install the WakaTime plugin using your preferred plugin manager:
</p>
<p class="text-lg">Install the WakaTime plugin using your preferred plugin manager:</p>
<div class="space-y-4">
<div class="bg-green/10 border border-green/30 rounded-lg p-4">
@ -151,14 +144,11 @@ git clone https://github.com/wakatime/vim-wakatime.git</code></pre>
Next Step
<% end %>
</div>
<% elsif params[:editor] == "neovim" %>
<section id="neovim" class="bg-dark rounded-lg p-6 mb-6">
<h3 class="text-2xl font-bold text-green mb-4">📟 Neovim</h3>
<div class="space-y-6">
<p class="text-lg">
Install the WakaTime plugin using your preferred plugin manager:
</p>
<p class="text-lg">Install the WakaTime plugin using your preferred plugin manager:</p>
<div class="space-y-4">
<div class="bg-green/10 border border-green/30 rounded-lg p-4">
@ -185,14 +175,11 @@ git clone https://github.com/wakatime/vim-wakatime.git</code></pre>
Next Step
<% end %>
</div>
<% elsif params[:editor] == "emacs" %>
<section id="emacs" class="bg-dark rounded-lg p-6 mb-6">
<h3 class="text-2xl font-bold text-purple mb-4">🔮 Emacs</h3>
<div class="space-y-6">
<p class="text-lg">
Install the WakaTime package using your preferred method:
</p>
<p class="text-lg">Install the WakaTime package using your preferred method:</p>
<div class="space-y-4">
<div class="bg-purple/10 border border-purple/30 rounded-lg p-4">
@ -217,14 +204,11 @@ git clone https://github.com/wakatime/vim-wakatime.git</code></pre>
Next Step
<% end %>
</div>
<% elsif params[:editor] == "godot" %>
<section id="godot" class="bg-dark rounded-lg p-6 mb-6">
<h3 class="text-2xl font-bold text-cyan mb-4">🎮 Godot</h3>
<div class="space-y-6">
<p class="text-lg">
Follow our comprehensive <a href="/docs/editors/godot" class="text-cyan hover:text-blue underline">Godot & Hackatime Setup guide</a> with video tutorial!
</p>
<p class="text-lg">Follow our comprehensive <a href="/docs/editors/godot" class="text-cyan hover:text-blue underline">Godot & Hackatime Setup guide</a> with video tutorial!</p>
<div class="bg-cyan/10 border border-cyan/30 rounded-lg p-4">
<h4 class="font-bold mb-2">Quick steps:</h4>
@ -235,15 +219,11 @@ git clone https://github.com/wakatime/vim-wakatime.git</code></pre>
<li>Click <strong>Download</strong> and <strong>Install</strong></li>
<li>Enable in <strong>Project → Project Settings → Plugins</strong></li>
</ol>
<p class="text-sm mt-3 text-cyan">
📺 <a href="https://www.youtube.com/watch?v=a938RgsBzNg&t=29s" target="_blank" class="underline">Watch the workshop recording</a> for a complete walkthrough!
</p>
<p class="text-sm mt-3 text-cyan">📺 <a href="https://www.youtube.com/watch?v=a938RgsBzNg&t=29s" target="_blank" class="underline">Watch the workshop recording</a> for a complete walkthrough!</p>
</div>
<div class="bg-yellow/10 border border-yellow/30 rounded-lg p-4">
<p class="text-yellow text-sm">
<strong>Note:</strong> You need to install the plugin for each Godot project separately (it's a Godot limitation).
</p>
<p class="text-yellow text-sm"><strong>Note:</strong> You need to install the plugin for each Godot project separately (it's a Godot limitation).</p>
</div>
</div>
</section>
@ -253,14 +233,11 @@ git clone https://github.com/wakatime/vim-wakatime.git</code></pre>
Next Step
<% end %>
</div>
<% else %>
<section class="bg-dark rounded-lg p-6 mb-6">
<h3 class="text-2xl font-bold text-orange mb-4">🔧 Setup your Editor</h3>
<div class="bg-orange/10 border border-orange/30 rounded-lg p-4 mb-4">
<p class="mb-4">
<strong>Hackatime works with any editor that supports WakaTime!</strong> This includes PyCharm, IntelliJ, Sublime Text, Atom, Neovim, Unity, Godot, and <a href="https://hackatime.hackclub.com/docs#supported-editors" class="text-cyan hover:text-blue underline">77+ more editors</a>.
</p>
<p class="mb-4"><strong>Hackatime works with any editor that supports WakaTime!</strong> This includes PyCharm, IntelliJ, Sublime Text, Atom, Neovim, Unity, Godot, and <a href="https://hackatime.hackclub.com/docs#supported-editors" class="text-cyan hover:text-blue underline">77+ more editors</a>.</p>
</div>
<div class="space-y-4">
@ -303,100 +280,98 @@ git clone https://github.com/wakatime/vim-wakatime.git</code></pre>
</div>
<% if params[:editor] == "vscode" %>
<script>
document.addEventListener('turbo:load', function() {
const waitingState = document.getElementById('waiting-state');
const successState = document.getElementById('success-state');
const statusMessage = document.getElementById('status-message');
const pollStatus = document.getElementById('poll-status');
const statusPanel = document.getElementById('status-panel');
<script>
document.addEventListener("turbo:load", function () {
const waitingState = document.getElementById("waiting-state");
const successState = document.getElementById("success-state");
const statusMessage = document.getElementById("status-message");
const pollStatus = document.getElementById("poll-status");
const statusPanel = document.getElementById("status-panel");
let checkCount = 0;
const maxChecks = 120; // 10m (5s)
let checkCount = 0;
const maxChecks = 120; // 10m (5s)
const msg = [
"Open any code file and start typing!",
"Try editing some code in VS Code...",
"Type a few characters in your editor!",
"We're watching for your first keystroke...",
"Make any edit in VS Code to continue!"
];
const msg = ["Open any code file and start typing!", "Try editing some code in VS Code...", "Type a few characters in your editor!", "We're watching for your first keystroke...", "Make any edit in VS Code to continue!"];
function showSuccess(timeAgo, detectedEditor) {
waitingState.classList.add('hidden');
successState.classList.remove('hidden');
statusPanel.classList.remove('border-darkless');
statusPanel.classList.add('border-green');
document.getElementById('heartbeat-time-ago').textContent = timeAgo;
function showSuccess(timeAgo, detectedEditor) {
waitingState.classList.add("hidden");
successState.classList.remove("hidden");
statusPanel.classList.remove("border-darkless");
statusPanel.classList.add("border-green");
document.getElementById("heartbeat-time-ago").textContent = timeAgo;
if (detectedEditor && detectedEditor.toLowerCase() !== 'vscode' && detectedEditor.toLowerCase() !== 'vs code') {
const mismatchMessage = document.getElementById('editor-mismatch-message');
mismatchMessage.textContent = `We detected a heartbeat from ${detectedEditor}. If this is intended, you're all set!`;
mismatchMessage.classList.remove('hidden');
if (detectedEditor && detectedEditor.toLowerCase() !== "vscode" && detectedEditor.toLowerCase() !== "vs code") {
const mismatchMessage = document.getElementById("editor-mismatch-message");
mismatchMessage.textContent = `We detected a heartbeat from ${detectedEditor}. If this is intended, you're all set!`;
mismatchMessage.classList.remove("hidden");
}
}
function check() {
fetch(<%= = api_v1_my_heartbeats_most_recent_path.to_json %>, {
headers: {
Authorization: "Bearer " + <%= = @current_user_api_key.to_json %>,
},
})
.then((response) => response.json())
.then((data) => {
if (data.has_heartbeat) {
const heartbeatTime = new Date(data.heartbeat.created_at);
const now = new Date();
const secondsAgo = (now - heartbeatTime) / 1000;
const recentThreshold = 86400;
if (secondsAgo <= recentThreshold) {
showSuccess(data.time_ago, data.editor);
return;
}
}
throw new Error("No recent heartbeats");
})
.catch((error) => {
checkCount++;
if (checkCount % 3 === 0) {
const msgIndex = Math.floor(checkCount / 3) % msg.length;
statusMessage.textContent = msg[msgIndex];
}
pollStatus.textContent = `Checked ${checkCount} time${checkCount === 1 ? "" : "s"}...`;
if (checkCount >= maxChecks) {
pollStatus.textContent = "Still waiting... Make sure the extension is installed!";
return;
}
setTimeout(check, 5000);
});
}
check();
window.skipToNext = function () {
window.location.href = <%= = my_wakatime_setup_step_4_path.to_json %>;
};
});
</script>
<style>
.spin {
width: 16px;
height: 16px;
border: 2px solid var(--color-darkless);
border-top: 2px solid var(--color-blue);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
function check() {
fetch(<%== api_v1_my_heartbeats_most_recent_path.to_json %>, {
headers: {
'Authorization': 'Bearer ' + <%== @current_user_api_key.to_json %>
}
})
.then(response => response.json())
.then(data => {
if (data.has_heartbeat) {
const heartbeatTime = new Date(data.heartbeat.created_at);
const now = new Date();
const secondsAgo = (now - heartbeatTime) / 1000;
const recentThreshold = 86400;
if (secondsAgo <= recentThreshold) {
showSuccess(data.time_ago, data.editor);
return;
}
}
throw new Error('No recent heartbeats');
})
.catch(error => {
checkCount++;
if (checkCount % 3 === 0) {
const msgIndex = Math.floor(checkCount / 3) % msg.length;
statusMessage.textContent = msg[msgIndex];
}
pollStatus.textContent = `Checked ${checkCount} time${checkCount === 1 ? '' : 's'}...`;
if (checkCount >= maxChecks) {
pollStatus.textContent = "Still waiting... Make sure the extension is installed!";
return;
}
setTimeout(check, 5000);
});
}
check();
window.skipToNext = function() {
window.location.href = <%== my_wakatime_setup_step_4_path.to_json %>;
};
});
</script>
<style>
.spin {
width: 16px;
height: 16px;
border: 2px solid var(--color-darkless);
border-top: 2px solid var(--color-blue);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</style>
<% end %>

View file

@ -31,23 +31,18 @@
<div class="text-center mb-8 mt-4 max-w-5xl mx-auto">
<h2 class="text-3xl font-bold mt-4 mb-4">Oh, and one more thing...</h2>
<p class="text-xl text-white mb-2">
<b>Please do not try to cheat the system!</b> We have measures in place to detect and prevent cheating. If you attempt to manipulate Hackatime, you will be banned from Hackatime and other participating YSWS / events / programs, so please play fair! We are a non-profit organization and we run off of donations.
</p>
<p class="text-xl text-white mb-2"><b>Please do not try to cheat the system!</b> We have measures in place to detect and prevent cheating. If you attempt to manipulate Hackatime, you will be banned from Hackatime and other participating YSWS / events / programs, so please play fair! We are a non-profit organization and we run off of donations.</p>
<label class="flex items-center justify-center gap-2 cursor-pointer select-none mt-2">
<input type="checkbox" id="o" class="w-5 h-5 rounded border-gray-300 text-primary focus:ring-primary bg-white">
<span class="text-xl text-primary">I agree and I understand the rules.</span>
</label>
<p class="text-xl text-white mt-2">
But besides that, you're all set! Happy hacking!
</p>
<p class="text-xl text-white mt-2">But besides that, you're all set! Happy hacking!</p>
</div>
<div class="text-center mb-8">
<div class="max-w-lg mx-auto">
<video src="<%= FlavorText.dino_meme_videos.sample %>" autoplay loop muted playsinline controls class="w-full rounded-lg">
</video>
<video src="<%= FlavorText.dino_meme_videos.sample %>" autoplay loop muted playsinline controls class="w-full rounded-lg"></video>
</div>
</div>
@ -58,7 +53,7 @@
<% if (url = session.dig(:return_data, "url")) %>
<%= link_to url, id: "s", class: "px-4 py-3 bg-primary hover:bg-primary/75 border border-darkless text-white rounded transition-colors cursor-pointer flex items-center justify-center opacity-50 cursor-not-allowed pointer-events-none" do %>
<%= session.dig(:return_data, "button_text") || "Done" %>
<%= session.dig(:return_data, 'button_text') || 'Done' %>
<% end %>
<% else %>
<%= link_to root_path, id: "s", class: "px-4 py-3 bg-primary hover:bg-primary/75 border border-darkless text-white rounded transition-colors cursor-pointer flex items-center justify-center opacity-50 cursor-not-allowed pointer-events-none" do %>
@ -67,13 +62,13 @@
<% end %>
<script>
document.getElementById('o').addEventListener('change', function() {
const x = document.getElementById('s');
document.getElementById("o").addEventListener("change", function () {
const x = document.getElementById("s");
if (this.checked) {
x.classList.remove('opacity-50', 'cursor-not-allowed', 'pointer-events-none');
x.classList.remove("opacity-50", "cursor-not-allowed", "pointer-events-none");
x.disabled = false;
} else {
x.classList.add('opacity-50', 'cursor-not-allowed', 'pointer-events-none');
x.classList.add("opacity-50", "cursor-not-allowed", "pointer-events-none");
x.disabled = true;
}
});