Skip to content
402 changes: 402 additions & 0 deletions cms/server/admin/static/aws_form_utils.js

Large diffs are not rendered by default.

222 changes: 222 additions & 0 deletions cms/server/admin/static/aws_table_utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/* Contest Management System
* Copyright © 2012-2014 Stefano Maggiolo <[email protected]>
* Copyright © 2012-2014 Luca Wehrstedt <[email protected]>
*
* Table sorting and filtering utilities for AWS.
* Extracted from aws_utils.js for better code organization.
*/

"use strict";

var CMS = CMS || {};
CMS.AWSTableUtils = CMS.AWSTableUtils || {};


/**
* Provides table row comparator for specified column and order.
*
* column_idx (int): Index of the column to sort by.
* numeric (boolean): Whether to sort numerically.
* ascending (boolean): Whether to sort in ascending order.
* return (function): Comparator function for Array.sort().
*/
CMS.AWSTableUtils.getRowComparator = function(column_idx, numeric, ascending) {
return function(a, b) {
var cellA = $(a).children("td").eq(column_idx);
var cellB = $(b).children("td").eq(column_idx);

// Use data-value if present, otherwise fallback to text
var valA = cellA.attr("data-value");
if (typeof valA === "undefined" || valA === "") valA = cellA.text().trim();

var valB = cellB.attr("data-value");
if (typeof valB === "undefined" || valB === "") valB = cellB.text().trim();

var result;
if (numeric) {
var numA = parseFloat(valA);
var numB = parseFloat(valB);

// Treat non-numeric/empty values so they always sink to bottom regardless of sort direction
if (isNaN(numA)) numA = ascending ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
if (isNaN(numB)) numB = ascending ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;

result = numA - numB;
return ascending ? result : -result;
} else {
result = valA.localeCompare(valB);
return ascending ? result : -result;
}
};
};


/**
* Sorts specified table by specified column in specified order.
*
* table (jQuery): The table element to sort.
* column_idx (int): Index of the column to sort by.
* ascending (boolean): Whether to sort in ascending order.
* header_element (Element): Optional header element for the column.
*/
CMS.AWSTableUtils.sortTable = function(table, column_idx, ascending, header_element) {
var initial_column_idx = table.data("initial_sort_column_idx");
var ranks_column = table.data("ranks_column");
var data_column_idx = column_idx + (ranks_column ? 1 : 0);
var table_rows = table
.children("tbody")
.children("tr");

// Use provided header element if available, otherwise find by index
var column_header;
if (header_element) {
column_header = $(header_element);
} else {
column_header = table
.children("thead")
.children("tr")
.children("th")
.eq(data_column_idx);
}
var settings = (column_header.attr("data-sort-settings") || "").split(" ");

var numeric = settings.indexOf("numeric") >= 0;

// If specified, flip column's natural order, e.g. due to meaning of values.
if (settings.indexOf("reversed") >= 0) {
ascending = !ascending;
}

// Normalize column index for data access, converting negative to positive from the end.
if (data_column_idx < 0) {
// For negative indices, calculate from the number of columns in data rows
var first_data_row = table_rows.first();
var num_cols = first_data_row.children("td,th").length;
data_column_idx = num_cols + data_column_idx;
}

// Reassign arrows to headers
table.find(".column-sort").html("&varr;");
column_header.find(".column-sort").html(ascending ? "&uarr;" : "&darr;");

// Do the sorting, by initial column and then by selected column.
table_rows
.sort(CMS.AWSTableUtils.getRowComparator(initial_column_idx, numeric, ascending))
.sort(CMS.AWSTableUtils.getRowComparator(data_column_idx, numeric, ascending))
.each(function(idx, row) {
table.children("tbody").append(row);
});

if (ranks_column) {
table_rows.each(function(idx, row) {
$(row).children("td").first().text(idx + 1);
});
}
};


/**
* Makes table sortable, adding ranks column and sorting buttons in header.
*
* table (jQuery): The table element to make sortable.
* ranks_column (boolean): Whether to add a ranks column.
* initial_column_idx (int): Index of the column to initially sort by.
* initial_ascending (boolean): Whether to initially sort in ascending order.
*/
CMS.AWSTableUtils.initTableSort = function(table, ranks_column, initial_column_idx, initial_ascending) {
table.addClass("sortable");
var table_column_headers = table
.children("thead")
.children("tr");
var table_rows = table
.children("tbody")
.children("tr");

// Normalize column index, converting negative to positive from the end.
initial_column_idx = table_column_headers
.children("th")
.eq(initial_column_idx)
.index();

table.data("ranks_column", ranks_column);
table.data("initial_sort_column_idx", initial_column_idx);

// Declaring sort settings.
var previous_column_idx = initial_column_idx;
var ascending = initial_ascending;

// Add sorting indicators to column headers
// Skip headers with the "no-sort" class
// Use data-sort-column attribute if present for correct column index
table_column_headers
.children("th")
.not(".no-sort")
.each(function(idx, header) {
var $header = $(header);
// Use data-sort-column if specified, otherwise use the header's index
var sortColumn = $header.data("sort-column");
if (sortColumn === undefined) {
sortColumn = $header.index();
}
$("<a/>", {
href: "#",
class: "column-sort",
click: function(e) {
e.preventDefault();
ascending = !ascending && previous_column_idx == sortColumn;
previous_column_idx = sortColumn;
CMS.AWSTableUtils.sortTable(table, sortColumn, ascending, header);
}
}).appendTo(header);
});

// Add ranks column
if (ranks_column) {
table_column_headers.prepend("<th>#</th>");
table_rows.prepend("<td></td>");
}

// Do initial sorting
CMS.AWSTableUtils.sortTable(table, initial_column_idx, initial_ascending);
};


/**
* Filters table rows based on search text.
*
* table_id (string): The id of the table to filter.
* search_text (string): The text to search for in table rows.
*/
CMS.AWSTableUtils.filterTable = function(table_id, search_text) {
var table = document.getElementById(table_id);
if (!table) {
return;
}
var rows = table.querySelectorAll("tbody tr");
var search_lower = search_text.toLowerCase().trim();

rows.forEach(function(row) {
if (search_lower === "") {
row.style.display = "";
return;
}
var text = row.textContent.toLowerCase();
if (text.indexOf(search_lower) !== -1) {
row.style.display = "";
} else {
row.style.display = "none";
}
});
};


// Backward compatibility aliases on CMS.AWSUtils
// These will be set up after aws_utils.js loads
$(document).ready(function() {
if (typeof CMS.AWSUtils !== 'undefined') {
// Alias the new functions to the old names for backward compatibility
CMS.AWSUtils.sort_table = CMS.AWSTableUtils.sortTable;
CMS.AWSUtils.init_table_sort = CMS.AWSTableUtils.initTableSort;
CMS.AWSUtils.filter_table = CMS.AWSTableUtils.filterTable;
}
});
140 changes: 139 additions & 1 deletion cms/server/admin/static/aws_tp_styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,111 @@
}

/* ==========================================================================
Add Student Dropdown/Modal
Native Dialog Element Styles
========================================================================== */

.tp-dialog {
border: none;
border-radius: 12px;
padding: 0;
box-shadow: var(--tp-shadow-lg);
max-width: 400px;
width: 90%;
overflow: visible;
}

/* Ensure dialog body can accommodate dropdown */
.tp-dialog .tp-dialog-body {
min-height: 200px;
}

.tp-dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
}

.tp-dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--tp-border);
}

.tp-dialog-header h3 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: var(--tp-text-primary);
}

.tp-dialog-close {
background: none;
border: none;
font-size: 1.5rem;
color: var(--tp-text-secondary);
cursor: pointer;
padding: 0;
line-height: 1;
}

.tp-dialog-close:hover {
color: var(--tp-text-primary);
}

.tp-dialog-body {
padding: 20px;
}

.tp-dialog-body label {
display: block;
font-size: 0.85rem;
font-weight: 600;
color: var(--tp-text-secondary);
margin-bottom: 8px;
}

.tp-dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 20px;
border-top: 1px solid var(--tp-border);
background: var(--tp-bg-light);
border-radius: 0 0 12px 12px;
}

/* ==========================================================================
Progress Link and Modern Progress Bar
========================================================================== */

.progress-link {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
}

.progress-link.no-tasks {
color: #9ca3af;
font-size: 0.85rem;
}

.progress-percentage {
font-weight: 600;
font-size: 0.9rem;
min-width: 45px;
/* Color based on percentage using CSS custom property */
color: hsl(calc(var(--pct, 0) * 1.2), 70%, 35%);
}

.progress-score {
font-size: 0.8rem;
color: #6b7280;
white-space: nowrap;
}

/* ==========================================================================
Legacy Add Student Dropdown (kept for backwards compatibility)
========================================================================== */

.add-student-dropdown {
Expand Down Expand Up @@ -739,6 +843,40 @@
background-image: linear-gradient(rgba(255,255,255,0.3), rgba(255,255,255,0.3));
}

/* Score cell coloring using CSS custom properties
Uses --score (0-100) to calculate hue: 0 (Red) -> 60 (Yellow) -> 120 (Green)
The hue is clamped between 0 and 120 using min/max */
.cell-content.score-cell {
--hue: calc(var(--score, 0) * 1.2);
background-color: hsl(min(120, max(0, var(--hue))), 75%, 90%);
color: hsl(min(120, max(0, var(--hue))), 90%, 20%);
background-image: none;
}

/* Modern progress bar using CSS custom properties
Uses --pct (0-100) to set width and color automatically */
.progress-bar-modern {
height: 6px;
background: var(--tp-border);
border-radius: 3px;
position: relative;
overflow: hidden;
width: 120px;
}

.progress-bar-modern::after {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: calc(var(--pct, 0) * 1%);
/* HSL color transitions from red (0) to green (120) based on percentage */
background: hsl(calc(var(--pct, 0) * 1.2), 70%, 45%);
border-radius: 3px;
transition: width 0.3s ease;
}

/* Hover effects */
.ranking-table tbody tr:hover td {
background-color: var(--tp-bg-hover);
Expand Down
Loading
Loading