mirror of
https://github.com/SrIzan10/hc-harbor.git
synced 2026-05-01 10:45:21 +00:00
Merge pull request #353 from hackclub/setting-refresh
settings redesign
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Hackatime!
|
||||
# Hackatime
|
||||
|
||||
[](https://status.hackatime.hackclub.com/status/hackatime)
|
||||
[](https://status.hackatime.hackclub.com/status/hackatime)
|
||||
@@ -48,9 +48,9 @@ app# bin/rails c # start an interactive irb!
|
||||
app# bin/rails db:migrate # migrate the database
|
||||
```
|
||||
|
||||
You can now access the app at http://localhost:3000/
|
||||
You can now access the app at <http://localhost:3000/>
|
||||
|
||||
Use email authentication from the homepage with `test@example.com` or create a new user (you can view outgoing emails at http://localhost:3000/letter_opener)!
|
||||
Use email authentication from the homepage with `test@example.com` or create a new user (you can view outgoing emails at [http://localhost:3000/letter_opener](http://localhost:3000/letter_opener))!
|
||||
|
||||
Ever need to setup a new database?
|
||||
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
.admin-timeline-view-wrapper {
|
||||
overflow-x: auto; /* Enables horizontal scrolling */
|
||||
width: 100%; /* Takes full width of its parent in the main layout */
|
||||
-webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
|
||||
flex-grow: 1; /* Allow it to take available vertical space in flex layouts */
|
||||
display: flex; /* Needed for flex-grow to work properly with its child */
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.admin-timeline-content-sizer {
|
||||
/* min-width is set inline dynamically via ERB based on number of users and min_column_width_px */
|
||||
display: block; /* Or inline-block; block should be fine to ensure it respects min-width */
|
||||
/* This element will be wider than its parent if content requires it, triggering scroll on admin-timeline-view-wrapper */
|
||||
}
|
||||
|
||||
.admin-timeline-sticky-header {
|
||||
/* Changed structure to use simple absolute positioning, no more flex layout */
|
||||
position: sticky; /* Sticky to the top of admin-timeline-view-wrapper */
|
||||
top: 0;
|
||||
z-index: 20; /* Above other content */
|
||||
background-color: #1F2937; /* Match page background to avoid transparency issues */
|
||||
padding-bottom: 0.5rem;
|
||||
padding-top: 0.25rem;
|
||||
box-sizing: border-box;
|
||||
/* No padding left/right as we use absolute positioning for the headers */
|
||||
}
|
||||
|
||||
/* Removed header spacer and container styles as they're no longer used */
|
||||
|
||||
.admin-timeline-user-header-cell {
|
||||
position: absolute; /* Positioned absolutely within the sticky header container */
|
||||
/* left is set inline per instance - using the EXACT same calculation as activity spans */
|
||||
top: 0; /* All headers start at the top of the container */
|
||||
min-width: 186px; /* Minimum width for each header cell */
|
||||
width: 186px; /* Fixed width set inline to match span columns exactly */
|
||||
box-sizing: border-box; /* Include padding and border in width/height */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
color: #E5E7EB;
|
||||
font-size: 0.875rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0.1rem 0.25rem; /* Padding for content */
|
||||
}
|
||||
|
||||
#timeline-grid-scroll-container { /* Original ID, holds hour rows and activity spans */
|
||||
overflow-y: auto; /* Retains vertical scroll for hours */
|
||||
position: relative; /* For absolute positioning of spans and current time line */
|
||||
flex-grow: 1; /* Takes remaining vertical space within admin-timeline-content-sizer */
|
||||
/* Width is implicitly 100% of admin-timeline-content-sizer, which can be very wide */
|
||||
}
|
||||
|
||||
/* Span items representing user activity */
|
||||
.admin-timeline-span-item {
|
||||
position: absolute;
|
||||
/* background-color is set by --span-color CSS variable, applied inline */
|
||||
color: #FFFFFF;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.1;
|
||||
padding: 2px 4px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
z-index: 10; /* Above grid lines */
|
||||
overflow: hidden; /* Clip content that overflows */
|
||||
box-sizing: border-box;
|
||||
min-width: 186px; /* Minimum width for each span "column" */
|
||||
width: 186px; /* Fixed width set inline to match exactly with headers */
|
||||
}
|
||||
|
||||
.admin-timeline-span-item a { /* Ensure links inside spans are visible and styled */
|
||||
color: inherit; /* Inherit color from parent span */
|
||||
text-decoration: underline;
|
||||
}
|
||||
.admin-timeline-span-item a:hover {
|
||||
color: #FACC15; /* Example: yellow hover for links */
|
||||
}
|
||||
|
||||
/* Styling for the hour rows that form the timeline background grid */
|
||||
.admin-timeline-hour-row {
|
||||
display: flex; /* To layout hour label container and gridline container */
|
||||
align-items: center;
|
||||
border-top: 1px solid #374151; /* Horizontal line for the hour */
|
||||
position: relative; /* For the absolutely positioned detailed grid line */
|
||||
box-sizing: border-box;
|
||||
/* height for each hour row is set inline via ERB (pixels_per_hour) */
|
||||
}
|
||||
|
||||
.admin-timeline-hour-label-container { /* Container for the hour text, e.g., "9:00 AM" */
|
||||
/* width for this container is set inline via ERB (line_left_rem) */
|
||||
flex-shrink: 0; /* Prevent shrinking */
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
color: #6B7280;
|
||||
padding-right: 0.5rem;
|
||||
text-align: right;
|
||||
box-sizing: border-box;
|
||||
height: 100%; /* Match parent row height */
|
||||
display: flex; /* To vertically center text if needed, though align-items on parent might suffice */
|
||||
align-items: flex-start; /* Vertically center text */
|
||||
}
|
||||
|
||||
.admin-timeline-hour-gridline-container { /* Container that stretches across for the faint mid-hour line */
|
||||
flex-grow: 1; /* Take remaining width */
|
||||
height: 100%; /* Fill the row height */
|
||||
position: relative; /* For the absolutely positioned line itself */
|
||||
}
|
||||
|
||||
.admin-timeline-hour-gridline { /* The faint horizontal line in the middle of an hour block */
|
||||
position: absolute;
|
||||
/* left: 0; right: (calculated from line_right_rem) are applied inline */
|
||||
top: 50%;
|
||||
border-bottom: 1px solid #374151; /* Color of the grid line */
|
||||
transform: translateY(-50%);
|
||||
z-index: 1; /* Behind activity spans */
|
||||
}
|
||||
|
||||
/* Current time indicator line */
|
||||
.admin-timeline-now-marker {
|
||||
position: absolute;
|
||||
/* left, right, top positions are set inline via ERB */
|
||||
height: 2px;
|
||||
background-color: #F87171; /* Prominent color for "now" line */
|
||||
z-index: 15; /* Above grid lines, potentially above spans if desired (adjust z-index accordingly) */
|
||||
}
|
||||
.admin-timeline-now-marker-text { /* For the "NOW" text label */
|
||||
position: absolute;
|
||||
left: -2.5rem; /* Adjust to position text to the left of the line_left_rem area */
|
||||
top: -0.4rem; /* Adjust to vertically center with the line */
|
||||
font-size: 0.65rem;
|
||||
color: #F87171; /* Match line color */
|
||||
background-color: #1F2937; /* Match page background to make it appear to "cut through" */
|
||||
padding: 0 0.2rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user-trust-red {
|
||||
background-color: rgba(239, 68, 68, 0.15) !important;
|
||||
border-left: 3px solid rgb(239, 68, 68) !important;
|
||||
}
|
||||
|
||||
.user-trust-green {
|
||||
background-color: rgba(16, 185, 129, 0.15) !important;
|
||||
border-left: 3px solid rgb(16, 185, 129) !important;
|
||||
}
|
||||
|
||||
.user-trust-yellow {
|
||||
background-color: rgba(245, 158, 11, 0.15) !important;
|
||||
border-left: 3px solid rgb(245, 158, 11) !important;
|
||||
}
|
||||
|
||||
.user-trust-blue {
|
||||
background-color: rgba(59, 130, 246, 0.1) !important;
|
||||
border-left: 3px solid rgb(59, 130, 246) !important;
|
||||
}
|
||||
|
||||
.user-trust-indicator {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.conviction-hammer {
|
||||
cursor: pointer;
|
||||
margin-left: 6px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.conviction-hammer:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.cm {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.cm-content {
|
||||
background-color: #1F2937;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.cm-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #374151;
|
||||
}
|
||||
|
||||
.cm-header h3 {
|
||||
margin: 0;
|
||||
color: #F3F4F6;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.cm-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #9CA3AF;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cm-body {
|
||||
padding: 16px;
|
||||
color: #D1D5DB;
|
||||
}
|
||||
|
||||
.cos {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.co {
|
||||
background-color: #2D3748;
|
||||
border: 1px solid #4B5563;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
color: #E5E7EB;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.co:hover {
|
||||
background-color: #374151;
|
||||
}
|
||||
@@ -10,7 +10,6 @@
|
||||
*/
|
||||
|
||||
@import "https://uchu.style/color.css";
|
||||
@import "https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css";
|
||||
@import "settings.css";
|
||||
|
||||
/* Force dark mode for all elements */
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
.settings-page .grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 400px), 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.settings-page article {
|
||||
background: var(--pico-card-background-color);
|
||||
border: var(--pico-border-width) solid var(--pico-card-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
box-shadow: var(--pico-card-box-shadow);
|
||||
transition: box-shadow var(--pico-transition);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-page article header {
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--pico-muted-border-color);
|
||||
}
|
||||
|
||||
.settings-page article header h2 {
|
||||
margin-bottom: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--pico-color);
|
||||
}
|
||||
|
||||
.settings-page article header p {
|
||||
margin-bottom: 0;
|
||||
color: var(--pico-muted-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.settings-page article section {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--pico-muted-border-color);
|
||||
}
|
||||
|
||||
.settings-page article section:first-of-type {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.settings-page article section h3 {
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--pico-color);
|
||||
}
|
||||
|
||||
.settings-page article .form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.settings-page article .form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.settings-page article button[role="button"],
|
||||
.settings-page article input[type="submit"][role="button"] {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.settings-page article .secondary {
|
||||
background-color: var(--pico-secondary-background);
|
||||
border-color: var(--pico-secondary-border);
|
||||
color: var(--pico-secondary-color);
|
||||
}
|
||||
|
||||
.settings-page article .secondary:hover {
|
||||
background-color: var(--pico-secondary-hover-background);
|
||||
border-color: var(--pico-secondary-hover-border);
|
||||
}
|
||||
|
||||
.settings-page article .code-example {
|
||||
margin: 1rem 0;
|
||||
background: var(--pico-code-background-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
}
|
||||
|
||||
.settings-page article .code-example pre {
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.settings-page article img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 1rem 0;
|
||||
border-radius: var(--pico-border-radius);
|
||||
}
|
||||
|
||||
.settings-page article pre:not(.code-example pre) {
|
||||
background: var(--pico-code-background-color);
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--pico-border-radius);
|
||||
font-size: 0.875rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.settings-page .mirror {
|
||||
padding: 1rem;
|
||||
background: var(--pico-card-sectioning-background-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.settings-page .mirror:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.settings-page .email-form {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--pico-muted-border-color);
|
||||
}
|
||||
|
||||
.settings-page .email-form .field {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.settings-page .email-form input[type="email"] {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.settings-page .grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.settings-page article {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.settings-page .email-form .field {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode styles (now default) */
|
||||
.settings-page article {
|
||||
background: var(--pico-card-background-color, #1e293b);
|
||||
border-color: var(--pico-card-border-color, #334155);
|
||||
}
|
||||
|
||||
.settings-page article:hover {
|
||||
box-shadow: var(
|
||||
--pico-card-box-shadow-hover,
|
||||
0 0.125rem 1rem rgba(0, 0, 0, 0.3)
|
||||
);
|
||||
}
|
||||
@@ -52,6 +52,9 @@
|
||||
.bg-dark {
|
||||
background-color: var(--color-dark);
|
||||
}
|
||||
.bg-darkless {
|
||||
background-color: var(--color-darkless);
|
||||
}
|
||||
.bg-sheet {
|
||||
background-color: var(--color-sheet);
|
||||
}
|
||||
|
||||
@@ -28,8 +28,7 @@ export default class extends Controller {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]')
|
||||
.content,
|
||||
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content,
|
||||
},
|
||||
body: JSON.stringify({ trust_level: trustLevel }),
|
||||
});
|
||||
@@ -40,10 +39,7 @@ export default class extends Controller {
|
||||
);
|
||||
}
|
||||
|
||||
// Update the current trust level in the dataset
|
||||
event.target.dataset.currentTrustLevel = trustLevel;
|
||||
|
||||
// Update the leaderboard entry's omitted class
|
||||
const leaderboardEntry = event.target.closest(".leaderboard-entry");
|
||||
if (leaderboardEntry) {
|
||||
if (trustLevel === "red") {
|
||||
|
||||
@@ -13,35 +13,10 @@
|
||||
num_users = users_data_array.count
|
||||
num_users = 1 if num_users == 0 # Ensure num_users is at least 1 for calculations
|
||||
|
||||
# Fixed REM values for layout (assuming 1rem = 16px for px calculations)
|
||||
line_left_rem = 4.0
|
||||
line_right_rem = 0.5
|
||||
activity_col_area_start_rem = line_left_rem # Header spacer aligns with hour labels
|
||||
activity_col_area_end_rem = line_right_rem # Header spacer aligns with right padding of grid
|
||||
|
||||
gutter_rem = 0.25
|
||||
pixels_per_hour = 128 # Controls vertical scale
|
||||
pixels_per_minute = pixels_per_hour / 60.0
|
||||
|
||||
min_column_width_px = 186 # Minimum width for each user column
|
||||
gutter_px = gutter_rem * 16 # Gutter in pixels
|
||||
|
||||
# Calculate the total minimum width required for all user columns + gutters + fixed label/padding areas
|
||||
# This width will be applied to the admin-timeline-content-sizer div
|
||||
user_columns_total_min_width_px = (num_users * min_column_width_px)
|
||||
gutters_total_width_px = (num_users > 1 ? (num_users - 1) * gutter_px : 0)
|
||||
|
||||
# Total min width for the content that scrolls (headers part)
|
||||
min_header_content_width_px = user_columns_total_min_width_px + gutters_total_width_px
|
||||
|
||||
# Total min width for the grid content part (timeline-grid-scroll-container's content)
|
||||
# This area includes the hour labels on the left, then the user activity columns, then right padding
|
||||
min_grid_content_width_px = (line_left_rem * 16) + min_header_content_width_px + (line_right_rem * 16)
|
||||
|
||||
# The sizer div needs to be at least as wide as the widest of its direct children (header row or grid row)
|
||||
# The header row's actual content part (user headers) spans min_header_content_width_px.
|
||||
# Add fixed spacers for the header:
|
||||
total_min_scroll_width_px = (activity_col_area_start_rem * 16) + min_header_content_width_px + (activity_col_area_end_rem * 16)
|
||||
gutter_px = 4
|
||||
|
||||
# Current admin user and selected users for Stimulus
|
||||
current_admin_user = {
|
||||
@@ -55,189 +30,179 @@
|
||||
current_date_for_form = @date.to_s
|
||||
%>
|
||||
|
||||
<div style="background-color: #1F2937; color: #FFFFFF; padding: 1rem; border-radius: 0.5rem; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; display: flex; flex-direction: column;">
|
||||
|
||||
<!-- User Selector UI -->
|
||||
<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 %>"
|
||||
style="margin-bottom: 1rem; padding: 0.75rem; background-color: #2D3748; border-radius: 0.375rem; flex-shrink: 0;"
|
||||
class="user-selector-compact"
|
||||
class="mb-4 p-3 bg-dark rounded-md flex-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">
|
||||
|
||||
<div class="grid">
|
||||
<div style="grid-column: span 7;">
|
||||
<label for="user-search-input" class="visually-hidden">Add User</label>
|
||||
<div style="position: relative;">
|
||||
<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"
|
||||
style="font-size: 0.875rem; padding: 0.35rem 0.5rem; margin-bottom: 0; width: 100%;">
|
||||
<ul class="list-group position-absolute w-100"
|
||||
data-admin-timeline-user-selector-target="searchResults"
|
||||
style="z-index: 1050; max-height: 200px; overflow-y: auto; list-style-type: none; padding-left: 0; margin-top: 0; width: 100%; background-color: #1A202C; border: 1px solid #4A5568; border-top: none; border-radius: 0 0 0.25rem 0.25rem; display: none;">
|
||||
<%# Search results will appear here %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="grid-column: span 5; display: flex; align-items: flex-end; justify-content: flex-end; gap: 0.5rem;">
|
||||
<div class="btn-group" role="group" style="display:contents;">
|
||||
<button type="button" class="secondary outline" data-action="admin-timeline-user-selector#applyPreset" data-period="today" style="font-size: 0.8rem; padding: 0.35rem 0.5rem; margin-bottom: 0;">Top Today</button>
|
||||
<button type="button" class="secondary outline" data-action="admin-timeline-user-selector#applyPreset" data-period="last_7_days" style="font-size: 0.8rem; padding: 0.35rem 0.5rem; margin-bottom: 0;">Top 7 Days</button>
|
||||
</div>
|
||||
<button type="submit" class="primary" style="font-size: 0.8rem; padding: 0.35rem 0.75rem; margin-bottom: 0;">View</button>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1 relative">
|
||||
<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 px-3 py-2 bg-darker rounded-md text-white placeholder-gray-300 focus:outline-none focus:border-transparent text-sm">
|
||||
<ul class="absolute top-full left-0 w-full bg-dark border border-gray-600 rounded-b-md z-50 max-h-48 overflow-y-auto hidden"
|
||||
data-admin-timeline-user-selector-target="searchResults">
|
||||
<%# Search results will appear here %>
|
||||
</ul>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div class="mt-2" data-admin-timeline-user-selector-target="selectedUsersContainer" style="margin-top: 0.5rem; min-height: 28px;">
|
||||
<div class="mt-2 min-h-7" data-admin-timeline-user-selector-target="selectedUsersContainer">
|
||||
<%# Selected user pills will appear here %>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Date Navigation (remains the same) -->
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; flex-shrink: 0;">
|
||||
<div style="font-size: 1.125rem; line-height: 1.75rem; font-weight: 600;">
|
||||
<div class="flex justify-between items-center mb-4 flex-shrink-0">
|
||||
<div class="text-lg font-semibold">
|
||||
<%= @date.in_time_zone(primary_user_tz).strftime("%A, %B %-d, %Y") %>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<%= link_to "← Prev", admin_timeline_path(date: @prev_date.to_s), class: "button button-outline", data: { "date-nav-link": "true" } %>
|
||||
<%= link_to "Today", admin_timeline_path(date: Time.current.to_date.to_s), class: "button button-outline", data: { "date-nav-link": "true" } %>
|
||||
<%= link_to "Next →", admin_timeline_path(date: @next_date.to_s), class: "button button-outline", data: { "date-nav-link": "true" } %>
|
||||
<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" } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Horizontal Scroll Wrapper -->
|
||||
<div class="admin-timeline-view-wrapper">
|
||||
<!-- Inner Content Sizer: This div gets the calculated min-width -->
|
||||
<div class="admin-timeline-content-sizer" style="min-width: <%= total_min_scroll_width_px %>px;">
|
||||
<div class="flex-1 overflow-x-auto overflow-y-auto">
|
||||
<% if users_data_array.any? %>
|
||||
<%
|
||||
hour_label_width = 80
|
||||
grid_width = hour_label_width + (users_data_array.count * (min_column_width_px + gutter_px))
|
||||
grid_height = 120 + (24 * pixels_per_hour)
|
||||
%>
|
||||
|
||||
<!-- User Headers Section (Sticky) - Using position: absolute for exact alignment -->
|
||||
<% if users_data_array.any? %>
|
||||
<div class="admin-timeline-sticky-header" style="padding-left: 0; padding-right: 0;">
|
||||
<div style="position: relative; width: 100%; height: 5rem;">
|
||||
<% users_data_array.each_with_index do |data, index| %>
|
||||
<% user = data[:user] %>
|
||||
<% total_coded_time_seconds = data[:total_coded_time] %>
|
||||
<%
|
||||
# Calculate the base offset for this column (without the hour label padding)
|
||||
# This ensures evenly spaced columns considering gutters
|
||||
base_offset_px = index * min_column_width_px + (index > 0 ? index * gutter_px : 0)
|
||||
|
||||
# Add the hour label padding to get the absolute left position - EXACT SAME CALCULATION AS SPANS
|
||||
header_left_px = (line_left_rem * 16) + base_offset_px
|
||||
%>
|
||||
<%
|
||||
trust_level_class = ""
|
||||
if user.respond_to?(:trust_level)
|
||||
case user.trust_level
|
||||
when "red"
|
||||
trust_level_class = "user-trust-red"
|
||||
when "green"
|
||||
trust_level_class = "user-trust-green"
|
||||
when "yellow"
|
||||
trust_level_class = "user-trust-yellow"
|
||||
end
|
||||
end
|
||||
%>
|
||||
<div class="admin-timeline-user-header-cell <%= trust_level_class %>"
|
||||
data-user-id="<%= user.id %>"
|
||||
data-controller="user-conviction"
|
||||
style="width: <%= min_column_width_px %>px; position: absolute; left: <%= header_left_px %>px;"
|
||||
title="User ID: <%= user.id %> - <%= user.respond_to?(:username) && user.username.present? ? user.username : 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: <%= user.timezone %>">
|
||||
<% github_url = user.respond_to?(:github_profile_url) ? user.github_profile_url : nil %>
|
||||
<%= render "shared/user_mention", user: user %>
|
||||
<div style="margin-top: 0.25rem; display: flex; gap: 0.5rem;">
|
||||
<% if current_user && user != current_user && user.slack_uid.present? %>
|
||||
<%= link_to "Slack", "slack://user?team=T0266FRGM&id=#{user.slack_uid}", target: "_blank", style: "font-size: 0.7rem; color: #9CA3AF; text-decoration: underline;" %>
|
||||
<% end %>
|
||||
<% if github_url.present? %>
|
||||
<%= link_to "Git", github_url, target: "_blank", style: "font-size: 0.7rem; color: #9CA3AF; text-decoration: underline;" %>
|
||||
<% end %>
|
||||
<% if current_user && current_user.admin? && user != current_user %>
|
||||
<a href="#"
|
||||
data-action="click->user-conviction#convictUser"
|
||||
data-user-id="<%= user.id %>"
|
||||
class="conviction-hammer"
|
||||
style="font-size: 0.7rem; color: #9CA3AF; text-decoration: none;"
|
||||
title="Set trust level">🔨</a>
|
||||
<% end %>
|
||||
<% if user.respond_to?(:trust_level) %>
|
||||
<span class="user-trust-indicator" data-user-conviction-target="trustIndicator" title="<%= user.trust_level.capitalize %>">
|
||||
<% case user.trust_level %>
|
||||
<% when "red" %>
|
||||
🔴
|
||||
<% when "green" %>
|
||||
🟢
|
||||
<% when "yellow" %>
|
||||
<20>
|
||||
<% when "blue" %>
|
||||
🔵
|
||||
<% end %>
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="user-trust-indicator" data-user-conviction-target="trustIndicator"></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if total_coded_time_seconds && total_coded_time_seconds > 0 %>
|
||||
<div style="font-size: 0.7rem; color: #cbd5e0; margin-top: 0.1rem; line-height: 1;">
|
||||
<%= short_time_simple(total_coded_time_seconds) %> coded
|
||||
</div>
|
||||
<% else %>
|
||||
<div style="font-size: 0.7rem; color: #718096; margin-top: 0.1rem; line-height: 1;">
|
||||
No time coded
|
||||
</div>
|
||||
<% end %>
|
||||
<div style="font-size: 0.75rem; color: #9CA3AF; margin-top: 0.125rem; line-height: 1.1;"><%= user.timezone %></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Timeline Grid and Spans Section -->
|
||||
<div id="timeline-grid-scroll-container" style="overflow: hidden;">
|
||||
<%# Hour markers and lines (background grid) %>
|
||||
<% (timeline_start_hour..timeline_end_hour).each do |hour| %>
|
||||
<div class="admin-timeline-hour-row" style="height: <%= pixels_per_hour %>px;">
|
||||
<div class="admin-timeline-hour-label-container" style="width: <%= line_left_rem %>rem;">
|
||||
<%= Time.utc(2000,1,1, hour).strftime("%-l:00 %p") %>
|
||||
</div>
|
||||
<div class="admin-timeline-hour-gridline-container">
|
||||
<%# The actual line is styled to span the container using absolute positioning from CSS %>
|
||||
<div class="admin-timeline-hour-gridline" style="left:0; right: <%= line_right_rem %>rem;"></div>
|
||||
<div class="relative" style="width: <%= grid_width %>px; height: <%= grid_height %>px;">
|
||||
<% (0..23).each do |hour| %>
|
||||
<%
|
||||
hour_top = 120 + (hour * pixels_per_hour)
|
||||
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-2 top-2 text-xs text-gray-300 font-mono px-1">
|
||||
<%= formatted_hour %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%# Current Time Line Indicator %>
|
||||
|
||||
<% users_data_array.each_with_index do |data, index| %>
|
||||
<%
|
||||
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_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
|
||||
%>
|
||||
<div class="absolute border-r border-gray-700"
|
||||
style="left: <%= column_left %>px; width: <%= min_column_width_px %>px; top: 120px; 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?(:username) && user.username.present? ? user.username : 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: <%= user.timezone %>">
|
||||
<div class="flex items-center space-x-1 mb-1">
|
||||
<%= 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>
|
||||
<% 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>
|
||||
<% end %>
|
||||
<div>
|
||||
<span class="text-sm"><%= trust_level_emoji %></span>
|
||||
</div>
|
||||
<% if current_user && current_user.admin? && user != current_user %>
|
||||
<div>
|
||||
<button data-action="openModal"
|
||||
data-user-id="<%= user.id %>"
|
||||
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" %>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-300">
|
||||
<%= user.timezone %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%
|
||||
current_time_in_zone = Time.current.in_time_zone(primary_user_tz)
|
||||
is_today = @date == Time.current.in_time_zone(primary_user_tz).to_date
|
||||
show_current_time_line = is_today && current_time_in_zone.hour >= timeline_start_hour && current_time_in_zone.hour < (timeline_end_hour + 1)
|
||||
|
||||
if show_current_time_line
|
||||
minutes_from_timeline_display_start_for_now = (current_time_in_zone.hour - timeline_start_hour) * 60 + current_time_in_zone.min
|
||||
current_time_line_top_px = (minutes_from_timeline_display_start_for_now * pixels_per_minute)
|
||||
minutes_from_timeline_start = (current_time_in_zone.hour - timeline_start_hour) * 60 + current_time_in_zone.min
|
||||
current_time_line_top_px = 120 + (minutes_from_timeline_start * pixels_per_minute)
|
||||
end
|
||||
%>
|
||||
<% if show_current_time_line %>
|
||||
<div class="admin-timeline-now-marker" style="left: <%= line_left_rem %>rem; right: <%= line_right_rem %>rem; top: <%= current_time_line_top_px %>px;">
|
||||
<div class="admin-timeline-now-marker-text">NOW</div>
|
||||
<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 %>
|
||||
|
||||
<%# Logic for calculating span properties (remains mostly the same) %>
|
||||
<%
|
||||
calculate_span_properties = lambda do |span_data, span_user_tz|
|
||||
return nil unless span_data && span_data[:start_time] && span_data[:duration]
|
||||
@@ -254,7 +219,7 @@
|
||||
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 = (minutes_from_view_start * pixels_per_minute).round
|
||||
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|
|
||||
@@ -282,37 +247,31 @@
|
||||
}
|
||||
end
|
||||
%>
|
||||
|
||||
<%# Activity Spans %>
|
||||
|
||||
<% users_data_array.each_with_index do |data, index| %>
|
||||
<%
|
||||
user = data[:user]
|
||||
user_spans = data[:spans]
|
||||
block_color = user_colors[index % user_colors.length]
|
||||
|
||||
# Calculate the base offset for this column (without the hour label padding)
|
||||
# This ensures evenly spaced columns considering gutters
|
||||
base_offset_px = index * min_column_width_px + (index > 0 ? index * gutter_px : 0)
|
||||
|
||||
# Add the hour label padding to get the absolute left position
|
||||
current_span_column_left_px = (line_left_rem * 16) + base_offset_px
|
||||
column_left = hour_label_width + (index * (min_column_width_px + gutter_px))
|
||||
%>
|
||||
<% Array(user_spans).each do |span_data| %>
|
||||
<% props = calculate_span_properties.call(span_data, user.timezone || primary_user_tz) %>
|
||||
<% next unless props %>
|
||||
<div class="admin-timeline-span-item"
|
||||
style="--span-color: <%= block_color %>;
|
||||
background-color: var(--span-color);
|
||||
left: <%= current_span_column_left_px %>px;
|
||||
width: <%= min_column_width_px %>px;
|
||||
<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] %>">
|
||||
<div style="font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<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" title="Open <%= project_detail[:name] %> on GitHub"><%= 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 <%= project_detail[:name] %> on GitHub"><%= project_detail[:name].truncate(20) %></a>
|
||||
<% else %>
|
||||
<span title="<%= project_detail[:name] %> - No GitHub Repo Mapped"><%= project_detail[:name].truncate(20) %> 🚫</span>
|
||||
<% end %>
|
||||
@@ -324,16 +283,15 @@
|
||||
<span>Coding Activity</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div style="font-size: 0.7rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<div class="truncate opacity-90">
|
||||
<%= props[:display_text_line2] %>
|
||||
</div>
|
||||
<div style="font-size: 0.7rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #E5E7EB;">
|
||||
<div class="truncate opacity-75">
|
||||
<%= props[:display_text_line3] %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% @timeline_commit_markers.each do |commit| %>
|
||||
<% user_index = users_data_array.find_index { |data| data[:user].id == commit[:user_id] } %>
|
||||
<% next unless user_index %>
|
||||
@@ -341,187 +299,153 @@
|
||||
<% user_tz = user&.timezone || primary_user_tz %>
|
||||
<% today_start_of_day_for_user = @date.in_time_zone(user_tz).beginning_of_day %>
|
||||
<% view_start_datetime = today_start_of_day_for_user.advance(hours: timeline_start_hour) %>
|
||||
<% base_offset_px = user_index * min_column_width_px + (user_index > 0 ? user_index * gutter_px : 0) %>
|
||||
<% pill_left_px = (line_left_rem * 16) + base_offset_px + min_column_width_px - 60 %> <!-- right align, adjust as needed -->
|
||||
<% column_left = hour_label_width + (user_index * (min_column_width_px + gutter_px)) %>
|
||||
<% 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 = (commit_minutes_from_view_start * pixels_per_minute).round %>
|
||||
<% 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="timeline-commit-pill"
|
||||
style="position: absolute;
|
||||
left: <%= pill_left_px %>px;
|
||||
top: <%= commit_top_px %>px;
|
||||
z-index: 20;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 0px 7px;
|
||||
font-size: 0.72em;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
text-decoration: none;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.10);">
|
||||
<span style="<%= commit[:additions].to_i > 0 ? 'color: #22c55e; font-weight: 600;' : '' %>">+<%= commit[:additions] %></span>
|
||||
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 style="<%= commit[:deletions].to_i > 0 ? 'color: #ef4444; font-weight: 600;' : '' %>">-<%= commit[:deletions] %></span>
|
||||
<span class="<%= commit[:deletions].to_i > 0 ? 'text-red-400 font-semibold' : '' %>">-<%= commit[:deletions] %></span>
|
||||
</a>
|
||||
<% end %>
|
||||
</div> <!-- End #timeline-grid-scroll-container -->
|
||||
</div> <!-- End .admin-timeline-content-sizer -->
|
||||
</div> <!-- End .admin-timeline-view-wrapper -->
|
||||
</div> <!-- End Main Page Container -->
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center h-64 text-gray-400">
|
||||
<div class="text-center">
|
||||
<div class="text-lg mb-2">No users selected</div>
|
||||
<div class="text-sm">Use the search bar above to add users to the timeline</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div id="trust-level-modal" class="fixed inset-0 items-center justify-center hidden z-50">
|
||||
<div class="bg-darker rounded-lg shadow-xl max-w-sm w-full mx-4">
|
||||
<div class="flex justify-between items-center p-4 border-b border-gray-800">
|
||||
<h3 class="text-base font-semibold text-gray-100">Set Trust Level</h3>
|
||||
<button id="close-modal" class="text-gray-400 hover:text-white text-xl">×</button>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div id="trust-level-status" class="hidden mb-4 p-3 rounded text-sm"></div>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<button class="trust-option w-full text-left px-3 py-2 rounded bg-darkless text-gray-100" data-level="green">
|
||||
🟢 Green - Trusted
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="trust-option w-full text-left px-3 py-2 rounded bg-darkless text-gray-100" data-level="yellow">
|
||||
🟡 Yellow - Suspected
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="trust-option w-full text-left px-3 py-2 rounded bg-darkless text-gray-100" data-level="red">
|
||||
🔴 Red - Convicted (banned)
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="trust-option w-full text-left px-3 py-2 rounded bg-darkless text-gray-100" data-level="blue">
|
||||
🔵 Blue - Unscored
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const modal = document.getElementById('trust-level-modal');
|
||||
const closeModal = document.getElementById('close-modal');
|
||||
const sdiv = document.getElementById('trust-level-status');
|
||||
let currentUserId = null;
|
||||
|
||||
function report(message, type = 'i') {
|
||||
sdiv.className = `mb-4 p-3 rounded-lg text-sm ${type === 's' ? 'bg-green-500/20 border border-green-500 text-green-300' :
|
||||
type === 'e' ? 'bg-red-500/20 border border-red-500 text-red-300' :
|
||||
'bg-blue-500/20 border border-blue-500 text-blue-300'}`;
|
||||
sdiv.textContent = message;
|
||||
sdiv.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hide() {
|
||||
sdiv.classList.add('hidden');
|
||||
}
|
||||
|
||||
function pause() {
|
||||
document.querySelectorAll('.trust-option').forEach(btn => btn.disabled = true);
|
||||
}
|
||||
|
||||
function resume() {
|
||||
document.querySelectorAll('.trust-option').forEach(btn => btn.disabled = false);
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('[data-action="openModal"]')) {
|
||||
e.preventDefault();
|
||||
currentUserId = e.target.dataset.userId;
|
||||
hide();
|
||||
resume();
|
||||
modal.classList.remove('hidden');
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
});
|
||||
|
||||
function close() {
|
||||
modal.classList.add('hidden');
|
||||
modal.style.display = 'none';
|
||||
hide();
|
||||
resume();
|
||||
}
|
||||
|
||||
closeModal.addEventListener('click', close);
|
||||
|
||||
modal.addEventListener('click', function(e) {
|
||||
if (e.target === modal) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('.trust-option').forEach(button => {
|
||||
button.addEventListener('click', async function() {
|
||||
const level = this.dataset.level;
|
||||
if (!currentUserId || !level) return;
|
||||
pause();
|
||||
report('Updating trust level...', 'i');
|
||||
|
||||
try {
|
||||
console.log("set trust", currentUserId, "to:", level);
|
||||
const url = new URL(
|
||||
`/users/${currentUserId}/update_trust_level`,
|
||||
window.location.origin
|
||||
);
|
||||
const response = await fetch(url, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify({ trust_level: level })
|
||||
});
|
||||
|
||||
|
||||
<% content_for :head do %>
|
||||
<style>
|
||||
.user-selector-compact .form-label-sm { font-size: 0.75rem; margin-bottom: 0.25rem; }
|
||||
.user-selector-compact input[type="text"] { font-size: 0.875rem; padding: 0.35rem 0.5rem; margin-bottom: 0; }
|
||||
.user-selector-compact button { font-size: 0.8rem; padding: 0.35rem 0.5rem; margin-bottom: 0; }
|
||||
|
||||
.user-selector-compact .list-group-item {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.4rem 0.75rem; /* Increased padding for easier clicking */
|
||||
cursor: pointer;
|
||||
background-color: #1A202C; /* Dark background for items */
|
||||
color: #E2E8F0; /* Light text color */
|
||||
border-bottom: 1px solid #2D3748; /* Separator */
|
||||
}
|
||||
.user-selector-compact .list-group.position-absolute {
|
||||
top: 100%; left: 0;
|
||||
border: 1px solid #4A5568; border-top: none;
|
||||
border-radius: 0 0 0.25rem 0.25rem;
|
||||
display: none !important; /* Hidden by default */
|
||||
}
|
||||
.user-selector-compact .list-group.active {
|
||||
display: block !important;
|
||||
}
|
||||
.user-selector-compact .list-group-item:hover { background-color: #4A5568; }
|
||||
.user-selector-compact .list-group-item.disabled { color: #A0AEC0; background-color: #2D3748; }
|
||||
|
||||
.user-selector-compact .avatar-xs { width: 20px; height: 20px; border-radius: 50%; vertical-align: middle; }
|
||||
.user-selector-compact .avatar-xxs { width: 16px; height: 16px; border-radius: 50%; vertical-align: middle; }
|
||||
|
||||
.user-selector-compact .user-pill {
|
||||
padding: 0.3em 0.6em; font-size: 0.8rem;
|
||||
display: inline-flex; align-items: center;
|
||||
border-radius: var(--pico-border-radius);
|
||||
margin-right: 0.25rem; margin-bottom: 0.25rem; /* Ensure pills wrap nicely */
|
||||
}
|
||||
.user-selector-compact .user-pill .btn-close-custom {
|
||||
background: none; border: none; color: inherit;
|
||||
padding: 0 0.25rem; margin-left: 0.4rem;
|
||||
font-size: 1rem; line-height: 1;
|
||||
cursor: pointer; opacity: 0.7;
|
||||
}
|
||||
.user-selector-compact .user-pill .btn-close-custom:hover { opacity: 1; }
|
||||
.visually-hidden {
|
||||
border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px;
|
||||
overflow: hidden; padding: 0; position: absolute; width: 1px;
|
||||
}
|
||||
|
||||
.user-trust-red {
|
||||
background-color: rgba(220, 38, 38, 0.15);
|
||||
border-left: 3px solid #DC2626;
|
||||
}
|
||||
|
||||
.user-trust-green {
|
||||
background-color: rgba(16, 185, 129, 0.15);
|
||||
border-left: 3px solid #10B981;
|
||||
}
|
||||
|
||||
.user-trust-yellow {
|
||||
background-color: rgba(245, 158, 11, 0.08);
|
||||
border-left: 3px solid #F59E0B;
|
||||
}
|
||||
|
||||
.user-trust-indicator {
|
||||
font-size: 0.75rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.cm {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.cm-content {
|
||||
background-color: #2D3748;
|
||||
border-radius: 0.5rem;
|
||||
width: 95%;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cm-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #4A5568;
|
||||
}
|
||||
|
||||
.cm-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.cm-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
color: #CBD5E0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cm-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.cos {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.co {
|
||||
background-color: #4A5568;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.75rem;
|
||||
color: white;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.tln {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background-color: #38A169;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
animation: f 3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes f {
|
||||
0% { opacity: 0; transform: translateY(20px); }
|
||||
10% { opacity: 1; transform: translateY(0); }
|
||||
90% { opacity: 1; transform: translateY(0); }
|
||||
100% { opacity: 0; transform: translateY(-20px); }
|
||||
}
|
||||
</style>
|
||||
<% end %>
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
report('done, reloading...', 's');
|
||||
close();
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
console.error('you done did it', error);
|
||||
report('check console', 'e');
|
||||
resume();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<div id="timezone_leaderboard_toggle">
|
||||
<p>
|
||||
<strong>Regional & Timezone Leaderboards</strong><br>
|
||||
<small>Access regional leaderboards that show users in your timezone region or specific timezone. Choose between timezone-specific, regional (UTC offset), or global competition modes.</small>
|
||||
</p>
|
||||
<%= form_with model: user, url: (@is_own_settings ? my_settings_path : settings_user_path(user)), method: :patch, local: false do |f| %>
|
||||
<%= f.check_box :default_timezone_leaderboard, checked: user.default_timezone_leaderboard, id: "user_default_timezone_leaderboard" %>
|
||||
<%= f.label :default_timezone_leaderboard, "Default to Timezone Leaderboard", for: "user_default_timezone_leaderboard" %>
|
||||
<%= f.submit "Save", role: "button" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= 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" %>
|
||||
</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" %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% if @user.api_keys.any? %>
|
||||
<pre>
|
||||
<pre class="bg-gray-800 rounded-lg text-sm text-gray-200 whitespace-pre-wrap break-all">
|
||||
# put this in your ~/.wakatime.cfg file
|
||||
|
||||
[settings]
|
||||
@@ -7,10 +7,9 @@ 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.
|
||||
No API keys found. Please migrate your keys from waka.hackclub.com below. New API key generation has yet to be implemented.
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
@@ -2,358 +2,385 @@
|
||||
<%= @is_own_settings ? "My Settings" : "Settings | #{@user.username}" %>
|
||||
<% end %>
|
||||
|
||||
<% content_for :body_class, "settings-page" %>
|
||||
|
||||
<main class="container">
|
||||
<header>
|
||||
<h1><%= @is_own_settings ? "My Settings" : "Settings for #{@user.username}" %></h1>
|
||||
<p>Change your settings for Hackatime and Sailors Log.</p>
|
||||
<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.username}" %>
|
||||
</h1>
|
||||
<p class="text-muted text-lg">Change your Hackatime experience and preferences</p>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="border border-primary rounded-xl p-6 hover:bg-gray-800/50 transition-all duration-200">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-red-600/10 rounded">
|
||||
<span class="text-2xl">🚀</span>
|
||||
</div>
|
||||
<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 font-medium rounded transition-colors duration-200" %>
|
||||
</div>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h2>🚀 Time tracking wizard</h2>
|
||||
</header>
|
||||
<p>Get started with tracking your coding time in just a few minutes.</p>
|
||||
<%= link_to "Set up time tracking", my_wakatime_setup_path, role: "button" %>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h2 id="user_timezone">🌍 Timezone</h2>
|
||||
</header>
|
||||
<div class="border border-primary rounded-xl p-6 hover:bg-gray-800/50 transition-all duration-200">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-red-600/10 rounded">
|
||||
<span class="text-2xl">🌍</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-white" id="user_timezone">Timezone</h2>
|
||||
</div>
|
||||
<%= form_with model: @user,
|
||||
url: @is_own_settings ? my_settings_path : settings_user_path(@user),
|
||||
method: :patch do |f| %>
|
||||
<div class="form-group">
|
||||
<%= f.label :timezone, "Your timezone" %>
|
||||
<%= f.select :timezone,
|
||||
method: :patch, local: false,
|
||||
class: "space-y-4" do |f| %>
|
||||
<div>
|
||||
<%= f.label :timezone, "Your 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: "form-select" %>
|
||||
{ include_blank: @user.timezone.blank? },
|
||||
{ class: "w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded text-white focus:border-primary focus:ring-1 focus:ring-primary" } %>
|
||||
</div>
|
||||
<small>This affects how your activity graph and other time-based features are displayed.</small>
|
||||
<%= f.submit "Save Settings", role: "button" %>
|
||||
<p class="text-xs text-gray-400">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 font-medium rounded transition-colors duration-200" %>
|
||||
<% end %>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h2 id="user_slack_status">💬 Slack Integration</h2>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<h3>Status Updates</h3>
|
||||
<p>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 to give permission to update your status", slack_auth_path, role: "button", class: "secondary" %>
|
||||
<% end %>
|
||||
<%= form_with model: @user,
|
||||
url: @is_own_settings ? my_settings_path : settings_user_path(@user),
|
||||
method: :patch do |f| %>
|
||||
<fieldset>
|
||||
<label>
|
||||
<%= f.check_box :uses_slack_status, id: "user_uses_slack_status" %>
|
||||
<%= f.label :uses_slack_status, "Update my Slack status with my current project" %>
|
||||
</label>
|
||||
</fieldset>
|
||||
<%= f.submit "Save Settings", role: "button" %>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 id="user_slack_notifications">Channel Notifications</h3>
|
||||
<% if @enabled_sailors_logs.any? %>
|
||||
<p>You have notifications enabled for the following channels:</p>
|
||||
<ul>
|
||||
<% @enabled_sailors_logs.each do |sl| %>
|
||||
<li>
|
||||
<%= render "shared/slack_channel_mention", channel_id: sl.slack_channel_id %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<p>You have no notifications enabled.</p>
|
||||
<% end %>
|
||||
<p>
|
||||
You can enable notifications for specific channels by running <code>/sailorslog on</code> in the Slack channel you want to enable notifications for.
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h2 id="user_github_account">🔗 Connected Accounts</h2>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<h3>GitHub Account</h3>
|
||||
<p>This is used to show your active projects on the leaderboard & current hacking activity on the dashboard.</p>
|
||||
<% if @user.github_uid.present? %>
|
||||
<p>✅ Your GitHub account is linked: <%= link_to "@#{@user.github_username}", "https://github.com/#{@user.github_username}", target: "_blank" %></p>
|
||||
<% if @user.github_access_token.present? %>
|
||||
<%= link_to "Relink GitHub Account", github_auth_path, data: { turbo: "false" }, role: "button" %>
|
||||
<%= link_to "Unlink GitHub Account", github_unlink_path,
|
||||
data: {
|
||||
turbo_method: :delete,
|
||||
confirm: "Are you sure? This will remove access to your GitHub data."
|
||||
},
|
||||
role: "button",
|
||||
class: "outline" %>
|
||||
<% else %>
|
||||
<p>⚠️ Your GitHub token has expired. Please relink your account.</p>
|
||||
<%= link_to "Relink GitHub Account", github_auth_path, data: { turbo: "false" }, role: "button" %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= link_to "Link GitHub Account", github_auth_path, data: { turbo: "false" }, role: "button" %>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 id="user_email_addresses">Email Addresses</h3>
|
||||
<p>These are the email addresses associated with your account.</p>
|
||||
<% if @user.email_addresses.any? %>
|
||||
<ul>
|
||||
<% @user.email_addresses.each do |email_address| %>
|
||||
<li>
|
||||
<%= email_address.email %>
|
||||
<% if email_address.source.present? %>
|
||||
<span class="super">
|
||||
(from <%= email_address.source.humanize %>)
|
||||
</span>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<p>No email addresses found.</p>
|
||||
<% end %>
|
||||
|
||||
<div class="email-form">
|
||||
<%= form_tag add_email_auth_path, data: { turbo: false } do %>
|
||||
<div class="field">
|
||||
<%= email_field_tag :email, nil, placeholder: "Add another email address", required: true %>
|
||||
</div>
|
||||
<%= submit_tag "Add Email", role: "button", class: "secondary" %>
|
||||
<% end %>
|
||||
<div class="border border-primary rounded-xl p-6 hover:bg-gray-800/50 transition-all duration-200">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-red-600/10 rounded">
|
||||
<span class="text-2xl">⚙️</span>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h2 id="user_hackatime_extension">⚙️ Extension Settings</h2>
|
||||
</header>
|
||||
<h2 class="text-xl font-semibold text-white" id="user_hackatime_extension">Extension Settings</h2>
|
||||
</div>
|
||||
<%= form_with model: @user,
|
||||
url: @is_own_settings ? my_settings_path : settings_user_path(@user),
|
||||
method: :patch do |f| %>
|
||||
<div class="form-group">
|
||||
<%= f.label "Simple text" %>
|
||||
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 { |type| [type.humanize, type] },
|
||||
selected: @user.hackatime_extension_text_type, class: "form-select" %>
|
||||
User.hackatime_extension_text_types.keys.map { |key| [key.humanize, key] },
|
||||
{},
|
||||
{ class: "w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded text-white focus:border-primary focus:ring-1 focus:ring-primary" } %>
|
||||
</div>
|
||||
<%= f.submit "Save Settings", role: "button" %>
|
||||
<%= f.submit "Save Settings", class: "w-full px-4 py-2 bg-primary text-white font-medium rounded transition-colors duration-200" %>
|
||||
<% end %>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h2 id="user_stats_badges">📊 Stats Badges</h2>
|
||||
</header>
|
||||
<section>
|
||||
<p>Show your coding stats on your GitHub profile with beautiful badges.</p>
|
||||
<h3>General Stats Badge</h3>
|
||||
<p>This badge shows your overall coding statistics.</p>
|
||||
<select name="theme" id="theme-select" onchange="updateBadgeTheme(this.value)" class="form-select">
|
||||
<% GithubReadmeStats.themes.each do |theme| %>
|
||||
<option value="<%= theme %>"><%= theme.humanize %></option>
|
||||
<div class="border border-primary rounded-xl p-6 hover:bg-gray-800/50 transition-all duration-200">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-red-600/10 rounded">
|
||||
<span class="text-2xl">💬</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-white" id="user_slack_status">Slack Integration</h2>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<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-gray-700 hover:bg-gray-600 text-gray-200 text-sm font-medium rounded transition-colors duration-200 mb-3" %>
|
||||
<% end %>
|
||||
</select>
|
||||
<% gh_badge = GithubReadmeStats.new(current_user.id, "darcula") %>
|
||||
<img id="badge-preview" src="<%= gh_badge.generate_badge_url %>" data-url="<%= gh_badge.generate_badge_url %>">
|
||||
<pre id="badge-url"><%= gh_badge.generate_badge_url %></pre>
|
||||
</section>
|
||||
<%= 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-gray-600 rounded focus:ring-primary bg-gray-800" %>
|
||||
<%= 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 font-medium rounded transition-colors duration-200" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if @projects.any? && @user.slack_uid.present? %>
|
||||
<section>
|
||||
<h3>Project Stats Badge</h3>
|
||||
<p>This badge shows individual project statistics.</p>
|
||||
<p><small>See <a href="https://github.com/pbhak/hackatime-badge">the documentation</a> for more customization options!</small></p>
|
||||
<select name="project-id" id="project-select" onchange="updateBadgeProject(this.value)" class="form-select">
|
||||
<% @projects.each do |project_name| %>
|
||||
<option value="<%= project_name %>"><%= project_name %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
<img id="work-time-badge-preview" src="<%= @work_time_stats_url %>" data-url="<%= @work_time_stats_url %>">
|
||||
<pre id="work-time-badge-url"><%= @work_time_stats_url %></pre>
|
||||
</section>
|
||||
<div class="border-t border-gray-700 pt-4">
|
||||
<h3 class="text-lg font-medium text-white mb-2" id="user_slack_notifications">Channel Notifications</h3>
|
||||
<% if @enabled_sailors_logs.any? %>
|
||||
<p class="text-gray-300 text-sm mb-2">You have notifications enabled for the following channels:</p>
|
||||
<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-gray-800 rounded">
|
||||
<%= 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-gray-400">
|
||||
You can enable notifications for specific channels by running <code class="px-1 py-0.5 bg-gray-800 rounded text-gray-200">/sailorslog on</code> in the Slack channel.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-primary rounded-xl p-6 hover:bg-gray-800/50 transition-all duration-200">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-red-600/10 rounded">
|
||||
<span class="text-2xl">🔒</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-white" id="user_privacy">Privacy Settings</h2>
|
||||
</div>
|
||||
<%= 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 :allow_public_stats_lookup,
|
||||
class: "w-4 h-4 text-primary border-gray-600 rounded focus:ring-primary bg-gray-800" %>
|
||||
<%= f.label :allow_public_stats_lookup, "Allow public stats lookup",
|
||||
class: "text-sm text-gray-200" %>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400">When enabled, others can view your coding statistics through public APIs.</p>
|
||||
<%= f.submit "Save Settings", class: "w-full px-4 py-2 bg-primary text-white font-medium rounded transition-colors duration-200" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="border border-primary rounded-xl p-6 hover:bg-gray-800/50 transition-all duration-200">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-red-600/10 rounded">
|
||||
<span class="text-2xl">🏆</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-white" id="user_beta_features">Leaderboard Settings</h2>
|
||||
</div>
|
||||
<p class="text-gray-300 text-sm mb-4">Customize how you see the leaderboard</p>
|
||||
<%= render "timezone_leaderboard_toggle", user: @user %>
|
||||
</div>
|
||||
|
||||
<div class="border border-primary rounded-xl p-6 hover:bg-gray-800/50 transition-all duration-200 md:col-span-2">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-red-600/10 rounded">
|
||||
<span class="text-2xl">🔗</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-white" id="user_github_account">Connected Accounts</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">GitHub Account</h3>
|
||||
<p class="text-gray-300 text-sm">This is used to show your active projects on the leaderboard & current hacking activity on the dashboard.</p>
|
||||
<% if @user.github_uid.present? %>
|
||||
<div class="flex items-center gap-2 p-3 bg-gray-800 border border-gray-600 rounded">
|
||||
<span class="text-green-400">✅</span>
|
||||
<span class="text-gray-200 text-sm">Connected: <%= link_to "@#{@user.github_username}", "https://github.com/#{@user.github_username}", target: "_blank", class: "text-primary hover:text-primary/80 underline" %></span>
|
||||
</div>
|
||||
<% unless @user.github_access_token.present? %>
|
||||
<%= 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 text-sm font-medium rounded transition-colors duration-200" %>
|
||||
<% end %>
|
||||
<% 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 font-medium rounded transition-colors duration-200" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3" id="user_email_addresses">
|
||||
<h3 class="text-lg font-medium text-white">Email Addresses</h3>
|
||||
<p class="text-gray-300 text-sm">These are the email addresses associated with your account.</p>
|
||||
<% if @user.email_addresses.any? %>
|
||||
<div class="space-y-2">
|
||||
<% @user.email_addresses.each do |email| %>
|
||||
<div class="flex items-center gap-2 p-2 bg-gray-800 border border-gray-600 rounded">
|
||||
<span class="text-gray-300 text-sm"><%= email.email %></span>
|
||||
<span class="text-xs px-2 py-1 bg-gray-700 text-gray-200 rounded">
|
||||
<%= email.source&.humanize || "Unknown" %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-gray-400 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-gray-800 border border-gray-600 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/80 text-white text-sm font-medium rounded transition-colors duration-200" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-primary rounded-xl p-6 hover:bg-gray-800/50 transition-all duration-200">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-red-600/10 rounded">
|
||||
<span class="text-2xl">📊</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-white" id="user_stats_badges">Stats Badges</h2>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-white mb-2">General Stats Badge</h3>
|
||||
<p class="text-gray-300 text-sm mb-4">Show your coding stats on your GitHub profile with beautiful badges.</p>
|
||||
|
||||
<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 bg-gray-800 border border-gray-600 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") %>
|
||||
<div class="p-4 bg-gray-800 border border-gray-600 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-gray-300 bg-gray-900 p-2 rounded overflow-x-auto"><%= gh_badge.generate_badge_url %></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @projects.any? && @user.slack_uid.present? %>
|
||||
<div class="border-t border-gray-700 pt-4">
|
||||
<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 bg-gray-800 border border-gray-600 rounded text-white focus:border-primary focus:ring-1 focus:ring-primary">
|
||||
<% @projects.each do |project| %>
|
||||
<option value="<%= project %>"><%= project %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
<div class="mt-3 p-4 bg-gray-800 border border-gray-600 rounded">
|
||||
<img id="project-badge-preview" src="<%= @work_time_stats_url %>" class="mb-3 rounded">
|
||||
<pre id="project-badge-url" class="text-xs text-gray-300 bg-gray-900 p-2 rounded overflow-x-auto"><%= @work_time_stats_url %></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function updateBadgeTheme(theme) {
|
||||
const originalUrl = document.getElementById('badge-preview').dataset.url;
|
||||
const [baseUrl, queryString] = originalUrl.split('?');
|
||||
const params = queryString.split('&').map(param => {
|
||||
const [key, value] = param.split('=');
|
||||
return key === 'theme' ? `theme=${theme}` : param;
|
||||
});
|
||||
const newUrl = `${baseUrl}?${params.join('&')}`;
|
||||
document.getElementById('badge-preview').src = newUrl;
|
||||
document.getElementById('badge-url').textContent = newUrl;
|
||||
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;
|
||||
preview.src = newUrl;
|
||||
url.textContent = newUrl;
|
||||
}
|
||||
|
||||
function updateBadgeProject(project) {
|
||||
const originalUrl = document.getElementById('work-time-badge-preview').dataset.url;
|
||||
let splitUrl = originalUrl.split('/');
|
||||
splitUrl[splitUrl.length - 1] = project;
|
||||
const newUrl = splitUrl.join('/');
|
||||
document.getElementById('work-time-badge-preview').src = newUrl;
|
||||
document.getElementById('work-time-badge-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', '') %>';
|
||||
const newUrl = baseUrl + project;
|
||||
preview.src = newUrl;
|
||||
url.textContent = newUrl;
|
||||
}
|
||||
</script>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h2 id="user_markscribe">📝 Markscribe Templates</h2>
|
||||
<p>Use markscribe to create beautiful GitHub profile READMEs with your coding stats.</p>
|
||||
</header>
|
||||
|
||||
<div class="code-example">
|
||||
<pre><code>{{ wakatimeDoubleCategoryBar "💾 Languages:" wakatimeData.Languages "💼 Projects:" wakatimeData.Projects 5 }}</code></pre>
|
||||
</div>
|
||||
<p>Add this to your GitHub profile README template to display your top languages and projects.</p>
|
||||
<p><small>See the <a href="https://github.com/taciturnaxolotl/markscribe#your-wakatime-languages-formated-as-a-bar" target="_blank">markscribe documentation</a> for more template options.</small></p>
|
||||
<img src="https://cdn.fluff.pw/slackcdn/524e293aa09bc5f9115c0c29c18fb4bc.png" alt="Example of markscribe output showing coding language and project statistics" width="100%"/>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h2 id="user_config_file">📄 Config File</h2>
|
||||
<p>
|
||||
<% if current_user.most_recent_direct_entry_heartbeat %>
|
||||
Your last heartbeat was <%= time_ago_in_words current_user.most_recent_direct_entry_heartbeat.created_at %> ago.
|
||||
<% else %>
|
||||
You haven't sent any heartbeats yet directly to this platform.
|
||||
<% end %>
|
||||
<div class="border border-primary rounded-xl p-6 hover:bg-gray-800/50 transition-all duration-200 space-y-6">
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-red-600/10 rounded">
|
||||
<span class="text-2xl">📄</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-white" id="user_config_file">Config File</h2>
|
||||
</div>
|
||||
<p class="text-gray-300 text-sm mb-4">Your Wakatime configuration file for tracking coding time.</p>
|
||||
|
||||
<div class="bg-gray-800 border border-gray-600 rounded p-4 overflow-x-auto">
|
||||
<%= render "wakatime_config_display" %>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-2">
|
||||
This configuration file is automatically generated and updated when you make changes to your settings.
|
||||
</p>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-700 pt-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-red-600/10 rounded">
|
||||
<span class="text-2xl">🚚</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-white" id="user_migration_assistant">Migration Assistant</h2>
|
||||
</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 font-medium rounded transition-colors duration-200" %>
|
||||
|
||||
<% 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-gray-800 border border-gray-600 rounded text-xs text-gray-300">
|
||||
Job ID: <%= job.id %> - Status: <%= job.status %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-primary rounded-xl p-6 hover:bg-gray-800/50 transition-all duration-200 md:col-span-2">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-red-600/10 rounded">
|
||||
<span class="text-2xl">📝</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-white" id="user_markscribe">Markscribe Templates</h2>
|
||||
</div>
|
||||
<p class="text-gray-300 text-sm mb-4">Use markscribe to create beautiful GitHub profile READMEs with your coding stats.</p>
|
||||
|
||||
<%= render "wakatime_config_display" %>
|
||||
<p>
|
||||
<small>
|
||||
This file is located in <code>~/.wakatime.cfg</code> on your computer.
|
||||
You can configure it with <a href="https://github.com/wakatime/wakatime-cli/blob/develop/USAGE.md#ini-config-file">other settings</a> as well.
|
||||
</small>
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h2 id="user_privacy">🔒 Privacy Settings</h2>
|
||||
</header>
|
||||
|
||||
<%= form_with model: @user,
|
||||
url: @is_own_settings ? my_settings_path : settings_user_path(@user),
|
||||
method: :patch do |f| %>
|
||||
<fieldset>
|
||||
<label for="user_allow_public_stats_lookup">
|
||||
<%= f.check_box :allow_public_stats_lookup, id: "user_allow_public_stats_lookup" %>
|
||||
<%= f.label :allow_public_stats_lookup, "Allow others to look up my public coding stats via the API" %>
|
||||
</label>
|
||||
</fieldset>
|
||||
<%= f.submit "Save Settings", role: "button" %>
|
||||
<% end %>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h2 id="user_beta_features">🏆 Leaderboard settings</h2>
|
||||
<p>Customize how you see the leaderboard</p>
|
||||
</header>
|
||||
<%= render "timezone_leaderboard_toggle", user: @user %>
|
||||
</article>
|
||||
|
||||
<%#
|
||||
<article>
|
||||
<header>
|
||||
<h2 id="user_beta_features">🧪 Beta Features</h2>
|
||||
<p>Enable experimental features and help us test new functionality.</p>
|
||||
</header>
|
||||
</article>
|
||||
%>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div class="p-4 bg-gray-800 border border-gray-600 rounded mb-4 overflow-x-auto">
|
||||
<pre class="text-sm text-gray-200 whitespace-pre-wrap break-all"><code>{{ wakatimeDoubleCategoryBar "💾 Languages:" wakatimeData.Languages "💼 Projects:" wakatimeData.Projects 5 }}</code></pre>
|
||||
</div>
|
||||
<p class="text-gray-300 text-sm mb-2">Add this to your GitHub profile README template to display your top languages and projects.</p>
|
||||
<p class="text-xs text-gray-400">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-gray-600"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% admin_tool do %>
|
||||
<article>
|
||||
<header>
|
||||
<h2 id="wakatime_mirror">🔄 WakaTime Mirror</h2>
|
||||
<p>Mirror your coding activity to WakaTime.</p>
|
||||
</header>
|
||||
|
||||
<div class="p-6 md:col-span-2">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-red-600/10 rounded">
|
||||
<span class="text-2xl">🔧</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-white">WakaTime Mirrors</h2>
|
||||
</div>
|
||||
|
||||
<% if current_user.wakatime_mirrors.any? %>
|
||||
<div class="mirrors-list">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<% current_user.wakatime_mirrors.each do |mirror| %>
|
||||
<div class="mirror">
|
||||
<p>
|
||||
<strong>Endpoint:</strong> <%= mirror.endpoint_url %><br>
|
||||
<strong>Last synced:</strong> <%= mirror.last_synced_at ? time_ago_in_words(mirror.last_synced_at) + " ago" : "Never" %>
|
||||
</p>
|
||||
<%= button_to "Delete", user_wakatime_mirror_path(current_user, mirror), method: :delete, role: "button", class: "secondary", data: { confirm: "Are you sure?" } %>
|
||||
<div class="p-4 bg-gray-800 border border-gray-600 rounded">
|
||||
<h3 class="text-white font-medium"><%= mirror.name %></h3>
|
||||
<p class="text-gray-400 text-sm"><%= mirror.endpoint_url %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= form_with(model: [current_user, WakatimeMirror.new], local: true) do |f| %>
|
||||
<div class="form-group">
|
||||
<%= f.label :endpoint_url, "WakaTime API Endpoint" %>
|
||||
<%= f.text_field :endpoint_url, value: "https://wakatime.com/api/v1", placeholder: "https://wakatime.com/api/v1", class: "form-control" %>
|
||||
<%= form_with(model: [current_user, WakatimeMirror.new], local: true, class: "space-y-4") do |f| %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<%= f.label :name, class: "block text-sm font-medium text-gray-200 mb-2" %>
|
||||
<%= f.text_field :name, class: "w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded text-white focus:border-primary focus:ring-1 focus:ring-primary" %>
|
||||
</div>
|
||||
<div>
|
||||
<%= f.label :endpoint_url, class: "block text-sm font-medium text-gray-200 mb-2" %>
|
||||
<%= f.url_field :endpoint_url, class: "w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded text-white focus:border-primary focus:ring-1 focus:ring-primary" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<%= f.label :encrypted_api_key, "WakaTime API Key" %>
|
||||
<%= f.text_field :encrypted_api_key, placeholder: "Enter your WakaTime API key", class: "form-control" %>
|
||||
</div>
|
||||
<%= f.submit "Add Mirror", role: "button" %>
|
||||
<%= f.submit "Add Mirror", class: "px-4 py-2 bg-primary text-white font-medium rounded transition-colors duration-200" %>
|
||||
<% end %>
|
||||
</article>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h2 id="user_migration_assistant">🚚 Migration Assistant</h2>
|
||||
<p>This will migrate your heartbeats from waka.hackclub.com to this platform.</p>
|
||||
</header>
|
||||
|
||||
<%= button_to "Migrate heartbeats", my_settings_migrate_heartbeats_path, method: :post, role: "button" %>
|
||||
|
||||
<% if @heartbeats_migration_jobs.any? %>
|
||||
<section>
|
||||
<h3>Migration Jobs</h3>
|
||||
<ul>
|
||||
<% @heartbeats_migration_jobs.each do |job| %>
|
||||
<li>
|
||||
<% if job.finished_at && !job.error %>
|
||||
✅
|
||||
<% elsif job.finished_at && job.error %>
|
||||
❌
|
||||
<% else %>
|
||||
⏳
|
||||
<% end %>
|
||||
Job started at <%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %>
|
||||
<% if job.finished_at %>
|
||||
(and finished after <%= distance_of_time_in_words(job.finished_at - job.created_at) %>)
|
||||
<% end %>
|
||||
<% admin_tool('', 'span') do %>
|
||||
<%= link_to "View job", GoodJob::Engine.routes.url_helpers.job_path(job.id) %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
<% end %>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user