mirror of
https://github.com/SrIzan10/hc-harbor.git
synced 2026-05-01 10:45:21 +00:00
pull out picocss and redo admin timeline
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user