diff --git a/webapp/public/js/domjudge.js b/webapp/public/js/domjudge.js index 97dd01eb73..c0fe0496aa 100644 --- a/webapp/public/js/domjudge.js +++ b/webapp/public/js/domjudge.js @@ -246,9 +246,7 @@ function getHeartCol(row) { function getTeamname(row) { - var res = row.getAttribute("id"); - if ( res === null ) return res; - return res.replace(/^team:/, ''); + return row.getAttribute("data-team-id"); } function toggle(id, show) @@ -944,3 +942,54 @@ function initializeKeyboardShortcuts() { } }); } + +// Make sure the items in the desktop scoreboard fit +document.querySelectorAll(".desktop-scoreboard .forceWidth:not(.toolong)").forEach(el => { + if (el instanceof Element && el.scrollWidth > el.offsetWidth) { + el.classList.add("toolong"); + } +}); + +/** + * Helper method to resize mobile team names and problem badges + */ +function resizeMobileTeamNamesAndProblemBadges() { + // Make team names fit on the screen, but only when the mobile + // scoreboard is visible + const mobileScoreboard = document.querySelector('.mobile-scoreboard'); + if (mobileScoreboard.offsetWidth === 0) { + return; + } + const windowWidth = document.body.offsetWidth; + const teamNameMaxWidth = Math.max(10, windowWidth - 150); + const problemBadgesMaxWidth = Math.max(10, windowWidth - 78); + document.querySelectorAll(".mobile-scoreboard .forceWidth:not(.toolong)").forEach(el => { + el.classList.remove("toolong"); + el.style.maxWidth = teamNameMaxWidth + 'px'; + if (el instanceof Element && el.scrollWidth > el.offsetWidth) { + el.classList.add("toolong"); + } else { + el.classList.remove("toolong"); + } + }); + document.querySelectorAll(".mobile-scoreboard .mobile-problem-badges:not(.toolong)").forEach(el => { + el.classList.remove("toolong"); + el.style.maxWidth = problemBadgesMaxWidth + 'px'; + if (el instanceof Element && el.scrollWidth > el.offsetWidth) { + el.classList.add("toolong"); + const scale = el.offsetWidth / el.scrollWidth; + const offset = -1 * (el.scrollWidth - el.offsetWidth) / 2; + el.style.transform = `scale(${scale}) translateX(${offset}px)`; + } else { + el.classList.remove("toolong"); + el.style.transform = null; + } + }); +} + +$(function() { + if (document.querySelector('.mobile-scoreboard')) { + window.addEventListener('resize', resizeMobileTeamNamesAndProblemBadges); + resizeMobileTeamNamesAndProblemBadges(); + } +}); diff --git a/webapp/public/style_domjudge.css b/webapp/public/style_domjudge.css index e443f7271c..7b8db354a2 100644 --- a/webapp/public/style_domjudge.css +++ b/webapp/public/style_domjudge.css @@ -195,6 +195,9 @@ del { border-right: 1px solid silver; padding: 0; } +.scoreboard td.no-border, .scoreboard th.no-border { + border: none; +} .scoreboard td.score_cell { min-width: 4.2em; border-right: none; @@ -219,6 +222,13 @@ del { display: block; overflow: hidden; } + +.mobile-problem-badges { + position: relative; + display: block; + white-space: nowrap; +} + .toolong:after { content: ""; width: 30%; @@ -274,7 +284,7 @@ img.affiliation-logo { .silver-medal { background-color: #aaa } .bronze-medal { background-color: #c08e55 } -#scoresolv,#scoretotal { width: 2.5em; } +#scoresolv,#scoretotal,#scoresolvmobile,#scoretotalmobile { width: 2.5em; } .scorenc,.scorett,.scorepl { text-align: center; width: 2ex; } .scorenc { font-weight: bold; } td.scorenc { border-color: silver; border-right: 0; } @@ -699,3 +709,24 @@ blockquote { padding: 3px; border-radius: 5px; } + +.strike-diagonal { + position: relative; + text-align: center; +} + +.strike-diagonal:before { + position: absolute; + content: ""; + left: 0; + top: 50%; + right: 0; + border-top: 2px solid; + border-color: firebrick; + + -webkit-transform:rotate(-35deg); + -moz-transform:rotate(-35deg); + -ms-transform:rotate(-35deg); + -o-transform:rotate(-35deg); + transform:rotate(-35deg); +} diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index 2a73e324e4..a496f540be 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -23,6 +23,7 @@ use App\Service\DOMJudgeService; use App\Service\EventLogService; use App\Service\SubmissionService; +use App\Utils\Scoreboard\ScoreboardMatrixItem; use App\Utils\Utils; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\EntityManagerInterface; @@ -110,6 +111,7 @@ public function getFilters(): array new TwigFilter('fileTypeIcon', $this->fileTypeIcon(...)), new TwigFilter('problemBadge', $this->problemBadge(...), ['is_safe' => ['html']]), new TwigFilter('problemBadgeForContest', $this->problemBadgeForContest(...), ['is_safe' => ['html']]), + new TwigFilter('problemBadgeMaybe', $this->problemBadgeMaybe(...), ['is_safe' => ['html']]), new TwigFilter('printMetadata', $this->printMetadata(...), ['is_safe' => ['html']]), new TwigFilter('printWarningContent', $this->printWarningContent(...), ['is_safe' => ['html']]), new TwigFilter('entityIdBadge', $this->entityIdBadge(...), ['is_safe' => ['html']]), @@ -1091,6 +1093,45 @@ public function problemBadge(ContestProblem $problem, bool $grayedOut = false): ); } + public function problemBadgeMaybe(ContestProblem $problem, ScoreboardMatrixItem $matrixItem): string + { + $rgb = Utils::convertToHex($problem->getColor() ?? '#ffffff'); + if (!$matrixItem->isCorrect) { + $rgb = 'whitesmoke'; + } + $background = Utils::parseHexColor($rgb); + + // Pick a border that's a bit darker. + $darker = $background; + $darker[0] = max($darker[0] - 64, 0); + $darker[1] = max($darker[1] - 64, 0); + $darker[2] = max($darker[2] - 64, 0); + $border = Utils::rgbToHex($darker); + + // Pick the foreground text color based on the background color. + $foreground = ($background[0] + $background[1] + $background[2] > 450) ? '#000000' : '#ffffff'; + if (!$matrixItem->isCorrect) { + $foreground = 'silver'; + $border = 'linen'; + } + + $ret = sprintf( + '%s', + $rgb, + $border, + $foreground, + $problem->getShortname() + ); + if (!$matrixItem->isCorrect) { + if ($matrixItem->numSubmissionsPending > 0) { + $ret = '' . $ret . ''; + } elseif ($matrixItem->numSubmissions > 0) { + $ret = '' . $ret . ''; + } + } + return $ret; + } + public function problemBadgeForContest(Problem $problem, ?Contest $contest = null): string { $contest ??= $this->dj->getCurrentContest(); diff --git a/webapp/templates/partials/scoreboard.html.twig b/webapp/templates/partials/scoreboard.html.twig index 3c4f58e4e0..4fb8a18bfb 100644 --- a/webapp/templates/partials/scoreboard.html.twig +++ b/webapp/templates/partials/scoreboard.html.twig @@ -18,31 +18,37 @@ {% endif %}
-
- {{ current_contest.name }} - - {% if scoreboard is null %} - {{ current_contest | printContestStart }} - {% elseif scoreboard.freezeData.showFinal(jury) %} - {% if current_contest.finalizetime is empty %} - preliminary results - not final - {% else %} - final standings - {% endif %} - {% elseif scoreboard.freezeData.stopped %} - contest over, waiting for results - {% elseif static %} - {% set now = 'now'|date('U') %} - {{ current_contest.starttime | printelapsedminutes(now) }} - {% else %} - {% if current_contest.freezeData.started %} - started: +
+
+
+ {{ current_contest.name }} +
+
+ + {% if scoreboard is null %} + {{ current_contest | printContestStart }} + {% elseif scoreboard.freezeData.showFinal(jury) %} + {% if current_contest.finalizetime is empty %} + preliminary results - not final + {% else %} + final standings + {% endif %} + {% elseif scoreboard.freezeData.stopped %} + contest over, waiting for results + {% elseif static %} + {% set now = 'now'|date('U') %} + {{ current_contest.starttime | printelapsedminutes(now) }} {% else %} - starts: + {% if current_contest.freezeData.started %} + started: + {% else %} + starts: + {% endif %} + {{ current_contest.starttime | printtime }} - ends: {{ current_contest.endtime | printtime }} {% endif %} - {{ current_contest.starttime | printtime }} - ends: {{ current_contest.endtime | printtime }} - {% endif %} - + +
+
{% if static %} diff --git a/webapp/templates/partials/scoreboard_table.html.twig b/webapp/templates/partials/scoreboard_table.html.twig index b0f64f2eab..3d0d9f9607 100644 --- a/webapp/templates/partials/scoreboard_table.html.twig +++ b/webapp/templates/partials/scoreboard_table.html.twig @@ -28,27 +28,32 @@ {% endif %} - +
+ + {% set teamColspan = 2 %} + {% if showAffiliationLogos %} + {% set teamColspan = teamColspan + 1 %} + {% endif %} {# output table column groups (for the styles) #} {% if enable_ranking %} - + {% endif %} {% if showFlags %} - + {% else %} {% endif %} {% if showAffiliationLogos %} - + {% endif %} - + {% if enable_ranking %} - - + + {% endif %} @@ -58,12 +63,6 @@ {% endfor %} {% endif %} - - {% set teamColspan = 2 %} - {% if showAffiliationLogos %} - {% set teamColspan = teamColspan + 1 %} - {% endif %} - {% if enable_ranking %} @@ -134,7 +133,7 @@ {% else %} {% set color = score.team.category.color %} {% endif %} - + {% if enable_ranking %}
{# Only print rank when score is different from the previous team #} @@ -316,6 +315,185 @@
+ + {# output table column groups (for the styles) #} + + {% if enable_ranking %} + + {% endif %} + {% if showFlags %} + + {% else %} + + {% endif %} + {% if showAffiliationLogos %} + + {% endif %} + + + {% if enable_ranking %} + + + + {% endif %} + + + {% set teamColspan = 2 %} + {% if showAffiliationLogos %} + {% set teamColspan = teamColspan + 1 %} + {% endif %} + + + {% if enable_ranking %} + + {% endif %} + + {% if enable_ranking %} + + {% endif %} + + + + {% set previousSortOrder = -1 %} + {% set previousTeam = null %} + {% set backgroundColors = {"#FFFFFF": 1} %} + {% set medalCount = 0 %} + {% for score in scores %} + {% set classes = [] %} + {% if score.team.category.sortorder != previousSortOrder %} + {% set classes = classes | merge(['sortorderswitch']) %} + {% set previousSortOrder = score.team.category.sortorder %} + {% set previousTeam = null %} + {% endif %} + + {# process medal color #} + {% set medalColor = '' %} + {% if showLegends %} + {% set medalColor = score.team | medalType(contest, scoreboard) %} + {% endif %} + + {# check whether this is us, otherwise use category colour #} + {% if myTeamId is defined and myTeamId == score.team.teamid %} + {% set classes = classes | merge(['scorethisisme']) %} + {% set color = '#FFFF99' %} + {% else %} + {% set color = score.team.category.color %} + {% endif %} + + {% if enable_ranking %} + + {% endif %} + + {% if showAffiliationLogos %} + + {% endif %} + {% if color is null %} + {% set color = "#FFFFFF" %} + {% set colorClass = "_FFFFFF" %} + {% else %} + {% set colorClass = color | replace({"#": "_"}) %} + {% set backgroundColors = backgroundColors | merge({(color): 1}) %} + {% endif %} + + {% if enable_ranking %} + {% set totalTime = score.totalTime %} + {% if scoreInSeconds %} + {% set totalTime = totalTime | printTimeRelative %} + {% endif %} + {% set totalPoints = score.numPoints %} + + {% endif %} + + + {% if showAffiliationLogos %} + {% set problemSpan = 3 %} + {% else %} + {% set problemSpan = 2 %} + {% endif %} + + + {% endfor %} + +
rankteamscore
+ {# Only print rank when score is different from the previous team #} + {% if not displayRank %} + ? + {% elseif previousTeam is null or scoreboard.scores[previousTeam.teamid].rank != score.rank %} + {{ score.rank }} + {% else %} + {% endif %} + {% set previousTeam = score.team %} + + {% if showFlags %} + {% if score.team.affiliation %} + {% set link = null %} + {% if jury %} + {% set link = path('jury_team_affiliation', {'affilId': score.team.affiliation.affilid}) %} + {% endif %} + + {{ score.team.affiliation.country|countryFlag }} + + {% endif %} + {% endif %} + + {% if score.team.affiliation %} + {% set link = null %} + {% if jury %} + {% set link = path('jury_team_affiliation', {'affilId': score.team.affiliation.affilid}) %} + {% endif %} + + {% set affiliationId = score.team.affiliation.externalid %} + {% set affiliationImage = affiliationId | assetPath('affiliation') %} + {% if affiliationImage %} + {{ score.team.affiliation.name }} + {% else %} + {{ affiliationId }} + {% endif %} + + {% endif %} + + {% set link = null %} + {% set extra = null %} + {% if static %} + {% set link = '#' %} + {% set extra = 'data-bs-toggle="modal" data-bs-target="#team-modal-' ~ score.team.teamid ~ '"' %} + {% else %} + {% if jury %} + {% set link = path('jury_team', {teamId: score.team.teamid}) %} + {% elseif public %} + {% set link = path('public_team', {teamId: score.team.teamid}) %} + {% set extra = 'data-ajax-modal' %} + {% else %} + {% set link = path('team_team', {teamId: score.team.teamid}) %} + {% set extra = 'data-ajax-modal' %} + {% endif %} + {% endif %} + + + {% if false and usedCategories | length > 1 and scoreboard.bestInCategory(score.team, limitToTeamIds) %} + + {{ score.team.category.name }} + + {% endif %} + {{ score.team.effectiveName }} + + {% if showAffiliations %} + + {% if score.team.affiliation %} + {{ score.team.affiliation.name }} + {% endif %} + + {% endif %} + + {{ totalPoints }}
{{ totalTime }}
+ {% for problem in problems %} + {% set matrixItem = scoreboard.matrix[score.team.teamid][problem.probid] %} + {{ problem | problemBadgeMaybe(matrixItem) }} + {% endfor %} +
+ {% if static %} {% for score in scores %} {% embed 'partials/modal.html.twig' with {'modalId': 'team-modal-' ~ score.team.teamid} %} @@ -366,7 +544,7 @@ {% else %} {% set cellColors = {first: 'Solved first', correct: 'Solved', incorrect: 'Tried, incorrect', pending: 'Tried, pending', neutral: 'Untried'} %} {% endif %} - +
@@ -385,7 +563,7 @@ {% endif %} {% if medalsEnabled %} -
Cell colours
+
diff --git a/webapp/templates/public/scoreboard.html.twig b/webapp/templates/public/scoreboard.html.twig index 6ab323c152..e1f0dbb559 100644 --- a/webapp/templates/public/scoreboard.html.twig +++ b/webapp/templates/public/scoreboard.html.twig @@ -12,7 +12,7 @@ {% set bannerImage = globalBannerAssetPath() %} {% endif %} {% if bannerImage %} - + {% endif %}
@@ -53,6 +53,7 @@ {% if static and refresh is defined %} disableRefreshOnModal(); {% endif %} + resizeMobileTeamNamesAndProblemBadges(); }; {% if static and refresh is defined %}
Medals {% if not scoreboard.freezeData.showFinal %}(tentative){% endif %}