Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c406a8b
feat: introduce a countdown overlay with visual effects before the du…
notkanishk Feb 6, 2026
f50aef3
fix: harden duel system correctness and disconnect handling
psygos Feb 6, 2026
51a2e95
feat: seed leaderboard with OTP users and guard unauthenticated results
psygos Feb 6, 2026
370546a
Harden duel pipeline and push spectator updates
psygos Feb 6, 2026
795ce81
fix: resolve OTP auth race condition when socket not yet connected
psygos Feb 6, 2026
3d0b373
ui: remove ads, clean up EULA, grey out DNF participants like F1
psygos Feb 6, 2026
f7ac6a2
fix: clean up FSM, fix OTP auth guard, show opponent results
psygos Feb 6, 2026
1965dde
chore: remove debug state display (tribeStateDisplay)
psygos Feb 6, 2026
87ffddd
Merge pull request #51 from redbrickhacks/leaderboard-kanishk
notkanishk Feb 6, 2026
c62f202
feat: add admin endpoints for users, config, and leaderboard
psygos Feb 6, 2026
609f318
Merge branch 'working-persistent-storage-and-results' of https://gith…
psygos Feb 6, 2026
16cc18e
fix: prevent stale UI/state bleeding between duel rounds
psygos Feb 6, 2026
3056e1f
chore: populate OTP map with 62 participants from rbh_list
psygos Feb 7, 2026
ba6e36c
chore: add Ansh and Kaustubh to OTP map
psygos Feb 7, 2026
366a796
chore: add Test 1 and Test 2 dummy users to OTP map
psygos Feb 7, 2026
4394c8e
fix: harden duel flow against save failures and suppress ads
psygos Feb 7, 2026
f04afb9
Revert "fix: harden duel flow against save failures and suppress ads"
psygos Feb 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions frontend/src/html/pages/rbh/spectator-screen.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@
</div>
</div>

<!-- Countdown Overlay -->
<div class="countdownOverlay hidden" id="countdownOverlay">
<div class="countdownContent">
<div class="countdownLabel">Duel starting in</div>
<div class="countdownNumber" id="countdownNumber">10</div>
</div>
</div>

<!-- Duel View (hidden by default) -->
<div class="view duelView hidden" id="duelView">
<div class="content">
Expand Down
5 changes: 0 additions & 5 deletions frontend/src/html/pages/tribe.html
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,6 @@
credentials provided.
</li>
</ul>
<p class="eulaFooter">
View your
<a href="/profile" target="_blank">public profile</a>
for past results and stats.
</p>
</div>
<div class="eulaCountdown">
<div class="eulaCountdownMessage">Continuing in</div>
Expand Down
1 change: 0 additions & 1 deletion frontend/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
</div>
<div class="history"></div>
</div>
<div id="tribeStateDisplay"></div>
<div id="barTimerProgress" class="timerMain">
<div class="opacityWrapper" style="opacity: 0">
<div class="bar"></div>
Expand Down
158 changes: 156 additions & 2 deletions frontend/src/styles/rbh/spectator-screen.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,92 @@
}
}

// ============================================================
// COUNTDOWN OVERLAY
// ============================================================
.countdownOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(6px);
opacity: 1;
transition: opacity 0.4s ease-out;

&.hidden {
display: none !important;
}

&.fade-out {
opacity: 0;
}

&.fade-in {
opacity: 0;
}

.countdownContent {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}

.countdownLabel {
font-size: 2rem;
font-weight: 500;
color: var(--sub-color);
text-transform: uppercase;
letter-spacing: 0.2em;
}

.countdownNumber {
font-size: 15rem;
font-weight: 900;
color: var(--main-color);
line-height: 1;
font-variant-numeric: tabular-nums;
text-shadow: 0 0 60px
color-mix(in srgb, var(--main-color) 40%, transparent);
animation: countdownPulse 1s ease-in-out infinite;
}

@keyframes countdownPulse {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.9;
}
}
}

// ============================================================
// LEADERBOARD VIEW
// ============================================================
.leaderboardView {
height: 100%;

.content {
display: grid;
grid-template-columns: 1fr;
display: flex;
flex-direction: column;
gap: 2rem;
height: 100%;

.bigtitle {
font-size: 2em;
margin-bottom: 1em;
flex-shrink: 0;
.text {
color: var(--main-color);
}
Expand All @@ -61,20 +134,29 @@
.tableAndUser {
font-size: 1rem;
width: 100%;
// Allow this container to shrink so overflow works inside
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;

& > .divider {
height: 0.25rem;
width: 100%;
background: var(--sub-alt-color);
border-radius: calc(var(--roundness) / 2);
margin-bottom: 2em;
flex-shrink: 0;
}

.leaderboardTable {
width: 100%;
display: flex;
flex-direction: column;
gap: 0.5rem;
// Fill remaining space and allow children to overflow
flex: 1;
min-height: 0;

.leaderboardHeader,
.leaderboardRow {
Expand Down Expand Up @@ -122,13 +204,48 @@
font-size: 0.75em;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--sub-alt-color);
flex-shrink: 0;
}

.leaderboardBody {
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
// Enable vertical scrolling for large leaderboards
overflow-y: auto;
// Fill remaining space but don't exceed it
flex: 1;
min-height: 0;
// Small bottom padding so last row isn't flush against edge
padding-bottom: 0.5rem;

// Subtle custom scrollbar for dark theme
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;

&::-webkit-scrollbar {
width: 6px;
}

&::-webkit-scrollbar-track {
background: transparent;
margin: 0.25rem 0;
}

&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 3px;
transition: background 0.2s ease;

&:hover {
background: rgba(255, 255, 255, 0.25);
}

&:active {
background: var(--main-color);
}
}

.leaderboardRow {
background: linear-gradient(
Expand All @@ -146,6 +263,8 @@
transition:
background 0.3s ease,
border-color 0.3s ease;
// Prevent rows from shrinking when container is constrained
flex-shrink: 0;

&:hover {
background: linear-gradient(
Expand All @@ -156,6 +275,41 @@
border-color: rgba(255, 255, 255, 0.1);
}

&.tier-podium {
border-color: color-mix(
in srgb,
var(--main-color),
transparent 55%
);
background: linear-gradient(
135deg,
color-mix(in srgb, var(--main-color), transparent 88%) 0%,
rgba(255, 255, 255, 0.04) 100%
);
}

&.tier-contender {
border-left: 3px solid
color-mix(in srgb, var(--main-color), transparent 35%);
}

&.placeholder-summary {
opacity: 0.9;
border-style: dashed;
border-color: rgba(255, 255, 255, 0.2);
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.02) 0%,
rgba(255, 255, 255, 0.01) 100%
);

.placeholder-count {
font-weight: 700;
color: var(--main-color);
margin-right: 0.4rem;
}
}

.avatarNameBadge {
display: flex;
gap: 0.5em;
Expand Down
35 changes: 19 additions & 16 deletions frontend/src/styles/tribe.scss
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
#tribeStateDisplay {
position: fixed;
z-index: 999999999999999;
}

.pageTribe {
height: 100%;
display: grid;
Expand Down Expand Up @@ -723,6 +718,13 @@
&.faded {
opacity: 0.25;
}
&.dnf {
opacity: 0.35;
color: var(--sub-color);
.pos {
color: var(--sub-color);
}
}
.progressAndGraph {
width: 25%;
}
Expand Down Expand Up @@ -1026,17 +1028,6 @@
margin-bottom: 0.5rem;
}
}

a {
color: var(--main-color);
text-decoration: underline;
}
}

.eulaFooter {
color: var(--sub-color);
font-size: 0.85rem;
text-align: center;
}

.eulaCountdown {
Expand Down Expand Up @@ -1204,6 +1195,18 @@ body.duelFlowActive {
#bannerCenter {
display: none !important;
}

// Hide all ads
.ad,
.advertisement,
#ad-vertical-left-wrapper,
#ad-vertical-right-wrapper,
#ad-footer-wrapper,
#ad-footer-small-wrapper,
#ad-result-wrapper,
#ad-result-small-wrapper {
display: none !important;
}
}

// Suppress qsr dev warning globally (fires before duelFlowActive is set)
Expand Down
31 changes: 30 additions & 1 deletion frontend/src/ts/event-handlers/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,41 @@ import * as TestState from "../test/test-state";
import * as TribeState from "../tribe/tribe-state";
import { isAnyChatSuggestionVisible } from "../tribe/tribe-chat";
import * as Tribe from "../tribe/tribe";
import * as DuelState from "../tribe/duel/duel-state";

document.addEventListener("keydown", async (e) => {
if (PageTransition.get()) return;
if (e.key === undefined) return;

const pageTestActive: boolean = ActivePage.get() === "test";
const activePage = ActivePage.get();
const pageTestActive: boolean = activePage === "test";
const duelState = DuelState.getFlowState();
const duelHotkeysLocked =
(activePage === "test" ||
activePage === "tribe" ||
activePage === "waiting") &&
duelState !== "SYSTEM_SELECT" &&
duelState !== "OTP";

if (
duelHotkeysLocked &&
!isInputElementFocused() &&
(e.key === "Tab" || e.key === "Enter" || e.key === "Escape")
) {
e.preventDefault();
return;
}

if (
duelHotkeysLocked &&
e.key.toLowerCase() === "p" &&
(e.metaKey || e.ctrlKey) &&
e.shiftKey
) {
e.preventDefault();
return;
}

if (pageTestActive && !TestState.resultVisible && !isInputElementFocused()) {
const popupVisible: boolean = Misc.isAnyPopupVisible();
// this is nested because isAnyPopupVisible is a bit expensive
Expand Down
Loading