From 6019e6854d06216f443d3374753bd0e207104627 Mon Sep 17 00:00:00 2001 From: jenz Date: Wed, 30 Sep 2026 16:02:33 +0200 Subject: [PATCH] further changes to backend for adjusting points, showing timestamps, and how categories are listed. uploaded build of react and actual frontend source --- RaceTimer_2026/racetimer_endpoints/README.md | 7 +- .../racetimer_invalidation_audit.sql | 13 + .../src/main/java/racetimer/dto/Dto.java | 4 + .../main/java/racetimer/model/BoardEntry.java | 8 + .../main/java/racetimer/model/Category.java | 2 +- .../src/main/java/racetimer/rest/Views.java | 2 + .../main/java/racetimer/service/Points.java | 47 + .../racetimer/service/SnapshotBuilder.java | 63 +- .../src/test/java/racetimer/rest/ApiTest.java | 4 +- .../java/racetimer/service/PointsTest.java | 49 + .../service/SnapshotBuilderTest.java | 26 +- RaceTimer_2026/racetimer_react/.env.example | 2 + RaceTimer_2026/racetimer_react/.gitignore | 3 + RaceTimer_2026/racetimer_react/README.md | 60 + RaceTimer_2026/racetimer_react/index.html | 16 + .../racetimer_react/package-lock.json | 1819 +++++++++++++++++ RaceTimer_2026/racetimer_react/package.json | 25 + .../racetimer_react/public/favicon.ico | Bin 0 -> 3870 bytes RaceTimer_2026/racetimer_react/src/App.tsx | 36 + .../racetimer_react/src/api/client.ts | 96 + .../racetimer_react/src/api/types.ts | 182 ++ RaceTimer_2026/racetimer_react/src/auth.tsx | 69 + .../src/components/AdminToggle.tsx | 67 + .../racetimer_react/src/components/Bits.tsx | 188 ++ .../racetimer_react/src/components/Layout.tsx | 93 + .../racetimer_react/src/components/States.tsx | 45 + .../src/components/TimeChart.tsx | 145 ++ RaceTimer_2026/racetimer_react/src/format.ts | 74 + RaceTimer_2026/racetimer_react/src/main.tsx | 37 + .../src/pages/LeaderboardPage.tsx | 68 + .../racetimer_react/src/pages/MapPage.tsx | 340 +++ .../racetimer_react/src/pages/MapsPage.tsx | 92 + .../racetimer_react/src/pages/PlayerPage.tsx | 349 ++++ .../racetimer_react/src/pages/PointsPage.tsx | 67 + .../racetimer_react/src/pages/SearchPage.tsx | 70 + RaceTimer_2026/racetimer_react/src/server.tsx | 39 + RaceTimer_2026/racetimer_react/src/styles.css | 1043 ++++++++++ RaceTimer_2026/racetimer_react/tsconfig.json | 19 + RaceTimer_2026/racetimer_react/vite.config.ts | 8 + RaceTimer_2026/racetimer_react_2026_build.zip | Bin 0 -> 86106 bytes 40 files changed, 5256 insertions(+), 21 deletions(-) create mode 100644 RaceTimer_2026/racetimer_endpoints/racetimer_invalidation_audit.sql create mode 100644 RaceTimer_2026/racetimer_react/.env.example create mode 100644 RaceTimer_2026/racetimer_react/.gitignore create mode 100644 RaceTimer_2026/racetimer_react/README.md create mode 100644 RaceTimer_2026/racetimer_react/index.html create mode 100644 RaceTimer_2026/racetimer_react/package-lock.json create mode 100644 RaceTimer_2026/racetimer_react/package.json create mode 100644 RaceTimer_2026/racetimer_react/public/favicon.ico create mode 100644 RaceTimer_2026/racetimer_react/src/App.tsx create mode 100644 RaceTimer_2026/racetimer_react/src/api/client.ts create mode 100644 RaceTimer_2026/racetimer_react/src/api/types.ts create mode 100644 RaceTimer_2026/racetimer_react/src/auth.tsx create mode 100644 RaceTimer_2026/racetimer_react/src/components/AdminToggle.tsx create mode 100644 RaceTimer_2026/racetimer_react/src/components/Bits.tsx create mode 100644 RaceTimer_2026/racetimer_react/src/components/Layout.tsx create mode 100644 RaceTimer_2026/racetimer_react/src/components/States.tsx create mode 100644 RaceTimer_2026/racetimer_react/src/components/TimeChart.tsx create mode 100644 RaceTimer_2026/racetimer_react/src/format.ts create mode 100644 RaceTimer_2026/racetimer_react/src/main.tsx create mode 100644 RaceTimer_2026/racetimer_react/src/pages/LeaderboardPage.tsx create mode 100644 RaceTimer_2026/racetimer_react/src/pages/MapPage.tsx create mode 100644 RaceTimer_2026/racetimer_react/src/pages/MapsPage.tsx create mode 100644 RaceTimer_2026/racetimer_react/src/pages/PlayerPage.tsx create mode 100644 RaceTimer_2026/racetimer_react/src/pages/PointsPage.tsx create mode 100644 RaceTimer_2026/racetimer_react/src/pages/SearchPage.tsx create mode 100644 RaceTimer_2026/racetimer_react/src/server.tsx create mode 100644 RaceTimer_2026/racetimer_react/src/styles.css create mode 100644 RaceTimer_2026/racetimer_react/tsconfig.json create mode 100644 RaceTimer_2026/racetimer_react/vite.config.ts create mode 100644 RaceTimer_2026/racetimer_react_2026_build.zip diff --git a/RaceTimer_2026/racetimer_endpoints/README.md b/RaceTimer_2026/racetimer_endpoints/README.md index dde633e..8f4a93e 100644 --- a/RaceTimer_2026/racetimer_endpoints/README.md +++ b/RaceTimer_2026/racetimer_endpoints/README.md @@ -18,6 +18,10 @@ All paths below are under `https:///racetimer_endpoints-1.0/api/`. The settings path can also be given with `-Dracetimer.settings=/path/file.json` or the `RACETIMER_SETTINGS` environment variable. +## Category numbers + +`categoryNumber` is counted per map and stage. Categories with exactly the same `server_cvars` share a number, so the same settings on ZE1 and ZE2 are both Category 1 and are told apart by `serverTag`. Different settings are numbered in order of their lowest `cvars_hash`. Hidden servers (dev) never take a number away from ze1/ze2. + ## Points Points are calculated per category, from each player's **best valid** time only. Tied times share a position. @@ -27,7 +31,8 @@ Points are calculated per category, from each player's **best valid** time only. | Who gets points | Everyone while a board has fewer than 200 completions. From 200 on, only the faster half (200 → top 100, 210 → top 105). | | Base points | n − (position − 1): the fastest gets n (the number of completions), each position below gets one less. The slower half gets 0, so on big boards points drop from about n/2 straight to 0 at the cut. | | Bonus multiplier | Only on boards with 100+ completions. 4× at #1, sliding smoothly to 2× at the top-1% mark, then to 1× at the top-5% mark. | -| CLASSIC RACETIMER | Final points ÷ 10. | +| CLASSIC RACETIMER | Final points ÷ 10, rounded. | +| Small-board touch-ups | Everyone who gets points gets at least 1, and each of the top 10 places gets at least 1 point more than the place below it (ties share). This only ever raises points, and in practice only changes Classic boards: a 9-record Classic board gives 9, 8, 7 … 1 instead of 1, 1, 1, 1, 1, 0, 0, 0, 0. Raised entries have `pointsRaised: true`. | | Invalid category | Still shown with positions, but gives 0 points. The admin and time are public (`invalidation`). | | Invalid record | Still listed on the board in time order with `isInvalid: true`, position 0 and 0 points. Only that one run is removed from the ranking; the player's previous valid time counts again. The admin and time are public (`invalidation`). | | Servers | ZE1 and ZE2 points and ranks are separate. Categories from any other tag (e.g. `dev`) are hidden. | diff --git a/RaceTimer_2026/racetimer_endpoints/racetimer_invalidation_audit.sql b/RaceTimer_2026/racetimer_endpoints/racetimer_invalidation_audit.sql new file mode 100644 index 0000000..ec4ceb2 --- /dev/null +++ b/RaceTimer_2026/racetimer_endpoints/racetimer_invalidation_audit.sql @@ -0,0 +1,13 @@ +-- Stores which admin marked a record or category invalid, and when. +-- Run once on the racetimer database before deploying the new WAR. +-- Restoring (making it valid again) clears these columns. + +ALTER TABLE unloze_racetimer_css_2026.timer_records + ADD COLUMN invalidated_by_steam VARCHAR(32) NULL DEFAULT NULL AFTER is_invalid, + ADD COLUMN invalidated_by_name VARCHAR(128) CHARACTER SET utf8mb4 NULL DEFAULT NULL AFTER invalidated_by_steam, + ADD COLUMN invalidated_at TIMESTAMP NULL DEFAULT NULL AFTER invalidated_by_name; + +ALTER TABLE unloze_racetimer_css_2026.zone_categories + ADD COLUMN invalidated_by_steam VARCHAR(32) NULL DEFAULT NULL AFTER is_invalid, + ADD COLUMN invalidated_by_name VARCHAR(128) CHARACTER SET utf8mb4 NULL DEFAULT NULL AFTER invalidated_by_steam, + ADD COLUMN invalidated_at TIMESTAMP NULL DEFAULT NULL AFTER invalidated_by_name; diff --git a/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/dto/Dto.java b/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/dto/Dto.java index 2e8732c..4deec6f 100644 --- a/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/dto/Dto.java +++ b/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/dto/Dto.java @@ -140,6 +140,8 @@ public final class Dto { public int points; /** 1.0 = no bonus. Up to 4.0 for #1 on boards with 100+ completions. */ public double bonusMultiplier; + /** Points were raised so small Classic boards keep 1 point between the top 10 places (and at least 1). */ + public boolean pointsRaised; /** Unix seconds. Null for legacy records (date unknown). */ public Long recordedAt; public boolean isLegacy; @@ -189,6 +191,8 @@ public final class Dto { public int completions; public double bonusMultiplier; public int points; + /** See BoardEntryDTO.pointsRaised. */ + public boolean pointsRaised; public Long recordedAt; } diff --git a/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/model/BoardEntry.java b/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/model/BoardEntry.java index 2ab895e..d39588c 100644 --- a/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/model/BoardEntry.java +++ b/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/model/BoardEntry.java @@ -11,13 +11,21 @@ public final class BoardEntry { public final int position; public final int points; public final double multiplier; + /** Points were raised by the Classic touch-ups (minimum 1, top-10 places 1 apart). */ + public final boolean pointsRaised; public BoardEntry(RecordRow record, Category category, int position, int points, double multiplier) { + this(record, category, position, points, multiplier, false); + } + + public BoardEntry(RecordRow record, Category category, int position, int points, double multiplier, + boolean pointsRaised) { this.record = record; this.category = category; this.position = position; this.points = points; this.multiplier = multiplier; + this.pointsRaised = pointsRaised; } /** An invalidated run shown on the board: no position, no points. */ diff --git a/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/model/Category.java b/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/model/Category.java index 6d0fc0e..50f9206 100644 --- a/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/model/Category.java +++ b/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/model/Category.java @@ -22,7 +22,7 @@ public final class Category { /** Who invalidated this category and when; null if valid or unknown. */ public Invalidation invalidation; - /** "Category N" exactly as the plugin numbers it in-game. */ + /** "Category N" on the website. Same server_cvars on ZE1 and ZE2 share the number. */ public int number; /** Valid best per player, fastest first. */ public List entries = new ArrayList<>(); diff --git a/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/rest/Views.java b/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/rest/Views.java index 719f007..177e5b1 100644 --- a/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/rest/Views.java +++ b/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/rest/Views.java @@ -108,6 +108,7 @@ public final class Views { d.time = e.record.time; d.points = e.points; d.bonusMultiplier = e.multiplier; + d.pointsRaised = e.pointsRaised; d.isLegacy = e.category.legacy; d.recordedAt = e.category.legacy ? null : e.record.recordedAt; d.isInvalid = e.isInvalid(); @@ -146,6 +147,7 @@ public final class Views { d.completions = c.completions(); d.bonusMultiplier = e.multiplier; d.points = e.points; + d.pointsRaised = e.pointsRaised; d.recordedAt = c.legacy ? null : e.record.recordedAt; d.isInvalid = e.isInvalid(); d.invalidation = invalidation(e.isInvalid(), e.record.invalidation); diff --git a/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/service/Points.java b/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/service/Points.java index caa8547..564c3e2 100644 --- a/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/service/Points.java +++ b/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/service/Points.java @@ -15,11 +15,19 @@ package racetimer.service; * 4x at #1 down to 2x at the top-1% mark, then 2x down to 1x at the * top-5% mark, 1x after that. * 4. CLASSIC RACETIMER (migrated) categories: final points divided by 10. + * 5. Whole-board touch-ups (boardPoints), which only ever raise points and in + * practice only change Classic boards, where dividing by 10 and rounding + * would squash places together: + * - everyone who gets points (step 1) gets at least 1; + * - each of the top 10 places gets at least 1 point more than the place + * below it (tied times still share their points). */ public final class Points { public static final int HALF_CUTOFF_FROM = 200; public static final int BONUS_FROM = 100; + /** Places 1..TOP_GAP_PLACES are always at least one point apart. */ + public static final int TOP_GAP_PLACES = 10; private Points() { } @@ -62,6 +70,45 @@ public final class Points { } /** The multiplier as shown to players, e.g. 3.2 or 1.65. */ + /** + * Final points for a whole board. positions[i] is the position of the i-th + * entry, fastest first (ties repeat the same position). Applies step 5 on + * top of finalPoints. The result has the same length and order. + */ + public static int[] boardPoints(int[] positions, int n, boolean classic) { + int[] pts = new int[positions.length]; + int pointed = pointedCount(n); + for (int i = 0; i < positions.length; i++) { + int p = finalPoints(positions[i], n, classic); + if (positions[i] >= 1 && positions[i] <= pointed && p < 1) { + p = 1; + } + pts[i] = p; + } + // Walk up from the bottom, one group of equal positions at a time. + int below = 0; // points of the next lower place that earns points (0 = none) + int i = positions.length - 1; + while (i >= 0) { + int j = i; + while (j > 0 && positions[j - 1] == positions[i]) { + j--; + } + int pos = positions[i]; + if (pos >= 1 && pos <= pointed) { + int p = pts[i]; + if (pos <= TOP_GAP_PLACES && p < below + 1) { + p = below + 1; + } + for (int k = j; k <= i; k++) { + pts[k] = p; + } + below = p; + } + i = j - 1; + } + return pts; + } + public static double displayMultiplier(int position, int n) { return Math.round(multiplier(position, n) * 100.0) / 100.0; } diff --git a/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/service/SnapshotBuilder.java b/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/service/SnapshotBuilder.java index f94aec6..0070bb4 100644 --- a/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/service/SnapshotBuilder.java +++ b/RaceTimer_2026/racetimer_endpoints/src/main/java/racetimer/service/SnapshotBuilder.java @@ -68,6 +68,37 @@ public final class SnapshotBuilder { } }; + /** + * Numbers the categories of one map+stage. Categories with exactly the same + * server_cvars share a number, so the same settings on ZE1 and ZE2 are both + * "Category 1" (told apart by their server tag). Distinct settings are + * numbered by their lowest cvars_hash. Hidden servers (dev) never take a + * number away from ze1/ze2: they reuse a matching number or come after. + */ + void numberByCvars(List list) { + Collections.sort(list, new Comparator() { + @Override + public int compare(Category a, Category b) { + int x = Boolean.compare(!a.ranked, !b.ranked); // public servers first + if (x != 0) { + return x; + } + x = Long.compare(a.cvarsHash, b.cvarsHash); + return x != 0 ? x : Integer.compare(a.id, b.id); + } + }); + Map numbers = new HashMap<>(); + for (Category c : list) { + String key = c.serverCvars == null ? "" : c.serverCvars.trim(); + Integer n = numbers.get(key); + if (n == null) { + n = numbers.size() + 1; + numbers.put(key, n); + } + c.number = n; + } + } + private final Set rankedTags; private final String classicServerCvars; @@ -81,7 +112,7 @@ public final class SnapshotBuilder { Collection records, Map> badges) { - // --- Categories, numbered like the plugin: per map+stage, ordered by cvars_hash --- + // --- Categories, numbered per map+stage by their server settings --- Map categories = new HashMap<>(); Map> byMapStage = new HashMap<>(); for (CategoryRow r : categoryRows) { @@ -99,16 +130,7 @@ public final class SnapshotBuilder { list.add(c); } for (List list : byMapStage.values()) { - Collections.sort(list, new Comparator() { - @Override - public int compare(Category a, Category b) { - int x = Long.compare(a.cvarsHash, b.cvarsHash); - return x != 0 ? x : Integer.compare(a.id, b.id); - } - }); - for (int i = 0; i < list.size(); i++) { - list.get(i).number = i + 1; - } + numberByCvars(list); } // --- Players --- @@ -188,6 +210,7 @@ public final class SnapshotBuilder { Collections.sort(sorted, byTime); int n = sorted.size(); List entries = new ArrayList<>(n); + int[] positions = new int[n]; int position = 0; double previousTime = Double.NaN; for (int i = 0; i < n; i++) { @@ -196,9 +219,15 @@ public final class SnapshotBuilder { position = i + 1; // tied times share the position of the first one } previousTime = r.time; - int points = c.givesPoints() ? Points.finalPoints(position, n, c.legacy) : 0; - double mult = c.givesPoints() ? Points.displayMultiplier(position, n) : 1.0; - BoardEntry e = new BoardEntry(r, c, position, points, mult); + positions[i] = position; + } + int[] boardPoints = c.givesPoints() ? Points.boardPoints(positions, n, c.legacy) : new int[n]; + for (int i = 0; i < n; i++) { + RecordRow r = sorted.get(i); + int points = boardPoints[i]; + boolean raised = c.givesPoints() && points > Points.finalPoints(positions[i], n, c.legacy); + double mult = c.givesPoints() ? Points.displayMultiplier(positions[i], n) : 1.0; + BoardEntry e = new BoardEntry(r, c, positions[i], points, mult, raised); entries.add(e); PlayerInfo p = players.get(r.steamAuth); p.bests.add(e); @@ -292,7 +321,11 @@ public final class SnapshotBuilder { @Override public int compare(Category a, Category b) { int x = Integer.compare(a.stage, b.stage); - return x != 0 ? x : Integer.compare(a.number, b.number); + if (x != 0) { + return x; + } + x = Integer.compare(a.number, b.number); + return x != 0 ? x : a.serverTag.compareTo(b.serverTag); // same settings: ze1 before ze2 } }); for (Category c : publicCategories) { diff --git a/RaceTimer_2026/racetimer_endpoints/src/test/java/racetimer/rest/ApiTest.java b/RaceTimer_2026/racetimer_endpoints/src/test/java/racetimer/rest/ApiTest.java index f99be24..54b3d58 100644 --- a/RaceTimer_2026/racetimer_endpoints/src/test/java/racetimer/rest/ApiTest.java +++ b/RaceTimer_2026/racetimer_endpoints/src/test/java/racetimer/rest/ApiTest.java @@ -102,8 +102,8 @@ public class ApiTest { ContainerResponse r = call("GET", "timers/player/" + A, null, null); assertEquals(200, r.getStatus()); JsonObject p = json(r).getAsJsonObject(); - // CLASSIC board with 2 finishers: A is #1 -> 2 points / 10 = 0 (rounded) - assertEquals(0, p.get("PlayerPoints").getAsInt()); + // CLASSIC board with 2 finishers: 2/10 would round to 0, but #2 gets at least 1 and #1 one more + assertEquals(2, p.get("PlayerPoints").getAsInt()); assertEquals("ze1", p.get("server").getAsString()); assertEquals("76561198029832363", p.get("steamID64").getAsString()); assertEquals(1, p.getAsJsonObject("servers").getAsJsonObject("ze2").get("rank").getAsInt()); diff --git a/RaceTimer_2026/racetimer_endpoints/src/test/java/racetimer/service/PointsTest.java b/RaceTimer_2026/racetimer_endpoints/src/test/java/racetimer/service/PointsTest.java index d1a3a97..85dc6bf 100644 --- a/RaceTimer_2026/racetimer_endpoints/src/test/java/racetimer/service/PointsTest.java +++ b/RaceTimer_2026/racetimer_endpoints/src/test/java/racetimer/service/PointsTest.java @@ -1,5 +1,6 @@ package racetimer.service; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import org.junit.Test; @@ -72,6 +73,54 @@ public class PointsTest { } } + private static int[] seq(int n) { + int[] p = new int[n]; + for (int i = 0; i < n; i++) { + p[i] = i + 1; + } + return p; + } + + @Test + public void smallClassicBoardKeepsOnePointBetweenPlaces() { + // The real 9-record Classic board: was 1,1,1,1,1,0,0,0,0. + assertArrayEquals(new int[] {9, 8, 7, 6, 5, 4, 3, 2, 1}, Points.boardPoints(seq(9), 9, true)); + } + + @Test + public void onlyTopTenGetTheGapButEveryoneWithPointsGetsAtLeastOne() { + int[] p = Points.boardPoints(seq(30), 30, true); + // Places 11..30: rounded n/10 values, at least 1. + assertEquals(2, p[10]); + assertEquals(1, p[29]); + for (int i = 0; i < 10; i++) { + assertEquals("place " + (i + 1), p[i + 1] + 1 <= p[i], true); + } + assertEquals(12, p[0]); + } + + @Test + public void tiesShareRaisedPoints() { + int[] p = Points.boardPoints(new int[] {1, 2, 2, 4}, 4, true); + assertArrayEquals(new int[] {3, 2, 2, 1}, p); + } + + @Test + public void bigBoardsAndNewRecordsAreUnchanged() { + for (int n : new int[] {5, 150, 1000, 1470}) { + int[] pos = seq(n); + int[] newPts = Points.boardPoints(pos, n, false); + for (int i = 0; i < n; i++) { + assertEquals(Points.finalPoints(i + 1, n, false), newPts[i]); + } + } + int[] classic = Points.boardPoints(seq(1470), 1470, true); + for (int i = 0; i < 735; i++) { + assertEquals(Points.finalPoints(i + 1, 1470, true), classic[i]); + } + assertEquals(0, classic[735]); // past the 50% cut still 0 + } + @Test public void emptyBoard() { assertEquals(0, Points.pointedCount(0)); diff --git a/RaceTimer_2026/racetimer_endpoints/src/test/java/racetimer/service/SnapshotBuilderTest.java b/RaceTimer_2026/racetimer_endpoints/src/test/java/racetimer/service/SnapshotBuilderTest.java index 9e64441..84a5efa 100644 --- a/RaceTimer_2026/racetimer_endpoints/src/test/java/racetimer/service/SnapshotBuilderTest.java +++ b/RaceTimer_2026/racetimer_endpoints/src/test/java/racetimer/service/SnapshotBuilderTest.java @@ -5,6 +5,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import java.util.List; import org.junit.Test; import racetimer.TestData; import racetimer.model.BoardEntry; @@ -18,6 +19,27 @@ public class SnapshotBuilderTest { private static final String B = TestData.steam(2); private static final String C = TestData.steam(3); + @Test + public void sameCvarsOnZe1AndZe2ShareACategoryNumber() { + // The real ze_pirates_port_royal_v3_6 stage 1 rows. + String a = "sv_gravity 800, sv_enablebunnyhopping 1, sv_autobunnyhopping 0, sv_accelerate 5, sv_airaccelerate 10, " + + "tickrate 100, sv_friction 4, client_speed 1.00, client_gravity 1.00"; + String b = a.replace("sv_enablebunnyhopping 1", "sv_enablebunnyhopping 0"); + TestData d = new TestData() + .category(87, "ze_p", 1, "ze1", TestData.CLASSIC, -706992435L, false) + .category(1963, "ze_p", 1, "ze1", a, -1995318401L, false) + .category(4412, "ze_p", 1, "ze2", a, -1978540782L, false) + .category(4564, "ze_p", 1, "ze1", b, 113025096L, false); + Snapshot s = d.build(); + assertEquals(1, s.categories.get(1963).number); + assertEquals(1, s.categories.get(4412).number); // same settings, other server + assertEquals(2, s.categories.get(87).number); + assertEquals(3, s.categories.get(4564).number); + List shown = s.mapsByName.get("ze_p").stages.get(1); + assertEquals("ze1", shown.get(0).serverTag); // Category 1 ZE1, then Category 1 ZE2 + assertEquals("ze2", shown.get(1).serverTag); + } + @Test public void categoriesAreNumberedByCvarsHashAcrossServers() { TestData d = new TestData() @@ -29,8 +51,8 @@ public class SnapshotBuilderTest { Snapshot s = d.build(); assertEquals(1, s.categories.get(12).number); // most negative hash first assertEquals(2, s.categories.get(10).number); - assertEquals(3, s.categories.get(14).number); // dev still takes a number, like in-game - assertEquals(4, s.categories.get(11).number); + assertEquals(3, s.categories.get(11).number); + assertEquals(4, s.categories.get(14).number); // hidden dev server does not take a public number assertEquals(1, s.categories.get(13).number); // dev is hidden from the public map list assertEquals(3, s.mapsByName.get("ze_map").stages.get(1).size()); diff --git a/RaceTimer_2026/racetimer_react/.env.example b/RaceTimer_2026/racetimer_react/.env.example new file mode 100644 index 0000000..f212742 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/.env.example @@ -0,0 +1,2 @@ +# Base URL of the racetimer backend API (no trailing slash). +VITE_API_BASE=https://racebackend.unloze.com/racetimer_endpoints-1.0/api diff --git a/RaceTimer_2026/racetimer_react/.gitignore b/RaceTimer_2026/racetimer_react/.gitignore new file mode 100644 index 0000000..f633501 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.env.local diff --git a/RaceTimer_2026/racetimer_react/README.md b/RaceTimer_2026/racetimer_react/README.md new file mode 100644 index 0000000..f5dc4b9 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/README.md @@ -0,0 +1,60 @@ +# Racetimer website (2026) + +React 18 + TypeScript + Vite frontend for the UNLOZE racetimer. It reads everything from the new Java backend (`racetimer_endpoints`). + +## Pages + +| URL | What it shows | +|---|---| +| `#/leaderboard` | Players ranked by points, with a ZE1 / ZE2 switch. The choice is remembered in the browser. | +| `#/player/` | Profile with ZE1 and ZE2 points and ranks. **Records** tab: best time per category with place, bonus multiplier, points and date ("Legacy record" for Classic data). A faster run that was invalidated is listed too, marked **Run invalidated**. **Improvement history** tab: every improvement and how much faster it was, with a chart for one category. | +| `#/maps` | Every map with its stages, categories, servers and record counts. | +| `#/map/?stage=&category=` | Stage tabs and category cards. Categories with exactly the same server settings share a number on ZE1 and ZE2 ("Category 1 ZE1", "Category 1 ZE2"). Shows the category's server settings, highlighting values that differ from other categories of the stage. Leaderboard with gap to #1, bonus, points and the 50% points cut-off line. Invalidated runs stay in the list where their time places them, marked **Invalid**, with no position and 0 points. | +| `#/search?q=` | Players and maps matching the header search box. | +| `#/points` | Plain-language explanation of the points rules. | + +Old links still work: `#/mapboard` goes to `#/maps`, and `#/map//` opens that stage. + +Dates are shown in full, down to the second, in the visitor's own time zone. + +## Admins + +The footer has **Admin sign-in with Steam**. It goes to the backend (`/api/auth/steam/login`). The backend checks SourceBans and sends the admin back with `#token=…`. The site stores the token in the browser and removes it from the address bar. + +While signed in: +- a yellow bar shows who is signed in, with **Sign out** +- map leaderboards, profile records and profile history show **Invalidate** / **Restore** buttons for single runs +- the category panel has **Invalidate** / **Restore** for the whole category + +Every action asks for confirmation first. After a change, all pages reload their data. + +Everybody (signed in or not) sees which admin invalidated a run or category and when, under the Invalid tag. + +Set `frontendUrl` in the backend settings to this site's URL, or the Steam sign-in cannot find its way back. + +## Build and deploy + +Requires Node 18 or newer. + +```bash +npm install +npm run build # output in dist/ +``` + +Upload the contents of `dist/` to the web server. The site uses `#` URLs, so no server rewrite rules are needed, and it works from any folder. + +The backend URL defaults to `https://racebackend.unloze.com/racetimer_endpoints-1.0/api`. To use another one, create `.env.local` from `.env.example` before building: + +``` +VITE_API_BASE=https://your-backend/racetimer_endpoints-1.0/api +``` + +For development, `npm run dev` serves the site on http://localhost:5173 with live reload. + +## Code layout + +- `src/api/types.ts`: JSON shapes returned by the backend. Keep in sync with `racetimer/dto/Dto.java`. +- `src/api/client.ts`: all API calls and the login token. +- `src/pages/`: one file per page. +- `src/components/`: shared pieces (layout, chips, admin button, chart). +- `src/styles.css`: the dark theme. Colors are CSS variables at the top of the file. diff --git a/RaceTimer_2026/racetimer_react/index.html b/RaceTimer_2026/racetimer_react/index.html new file mode 100644 index 0000000..cf7b583 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + UNLOZE Racetimer + + +
+ + + diff --git a/RaceTimer_2026/racetimer_react/package-lock.json b/RaceTimer_2026/racetimer_react/package-lock.json new file mode 100644 index 0000000..c85a4c7 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/package-lock.json @@ -0,0 +1,1819 @@ +{ + "name": "racetimer_react", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "racetimer_react", + "version": "2.0.0", + "dependencies": { + "@tanstack/react-query": "^5.59.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.27.0" + }, + "devDependencies": { + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "^5.6.3", + "vite": "^5.4.10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.4", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.4.tgz", + "integrity": "sha512-q7j5geK7xs3UJSdm9/iytUNclBnLmYx1EnSeCFXHPeutdqgIMeFeHtUZgS3EhlKxdBEAu8OwtJCwmLrEzpSs7Q==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.5.tgz", + "integrity": "sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.5.tgz", + "integrity": "sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.5.tgz", + "integrity": "sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.5.tgz", + "integrity": "sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.5.tgz", + "integrity": "sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.5.tgz", + "integrity": "sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.5.tgz", + "integrity": "sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.5.tgz", + "integrity": "sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.5.tgz", + "integrity": "sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.5.tgz", + "integrity": "sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.5.tgz", + "integrity": "sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.5.tgz", + "integrity": "sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.5.tgz", + "integrity": "sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.5.tgz", + "integrity": "sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.5.tgz", + "integrity": "sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.5.tgz", + "integrity": "sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.5.tgz", + "integrity": "sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.5.tgz", + "integrity": "sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.5.tgz", + "integrity": "sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.5.tgz", + "integrity": "sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.5.tgz", + "integrity": "sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.5.tgz", + "integrity": "sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.5.tgz", + "integrity": "sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.5.tgz", + "integrity": "sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.5.tgz", + "integrity": "sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tanstack/query-core": { + "version": "5.104.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.104.0.tgz", + "integrity": "sha512-JrC2r/JQlXt7khBSdUpsgxNvybzOg+aITa+ARRMlP2AFo93Y8vIqz077rp+e61mJetSZ0T6AJbwW1JHer1vrPQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.104.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.104.0.tgz", + "integrity": "sha512-e1TZmDCQnWIfiDVryIHeA6Idj+Lfx1hORLOhXS/l5wcgvtVD4yYqbTDl378sGQKIi2cSgb0TEMC9cKK8GGoJEw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.104.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.26", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.26.tgz", + "integrity": "sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001813", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001813.tgz", + "integrity": "sha512-zfjJo4rM0+fUomGDBW/xcDjhIwz/210DGvip2MAMDZ8KHcRPnOHmEgHPZP0UHlZoxr8fYKVJOqcORhYQcG4FKQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.440", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.440.tgz", + "integrity": "sha512-SghDzqdJokdz8zP8YNlvS74+CwLRoUPDGvP/gFA+HrKVw+pOshUAgx8pXR37xl/u16zGae746rFNWvZ/22kU2Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.6", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.6.tgz", + "integrity": "sha512-5HfK7k5im7LTOB0EqCQmfvy4C13G92Ssj1VTmouTK3AJvyjKTnFuCV0vcMAD/JS+JC4DvDIBRrlAeJIFjh5VWg==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.6", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.6.tgz", + "integrity": "sha512-0RHKZz7wwffvkU+2MFVT2NnjK44ssLEV+m0CAJaS2Ksmorrwj7WxH00jO0SOCW26/tINUnJHToXblDs33I38YQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.4", + "react-router": "6.30.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/rollup": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.5.tgz", + "integrity": "sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.5", + "@rollup/rollup-android-arm64": "4.63.5", + "@rollup/rollup-darwin-arm64": "4.63.5", + "@rollup/rollup-darwin-x64": "4.63.5", + "@rollup/rollup-freebsd-arm64": "4.63.5", + "@rollup/rollup-freebsd-x64": "4.63.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.5", + "@rollup/rollup-linux-arm-musleabihf": "4.63.5", + "@rollup/rollup-linux-arm64-gnu": "4.63.5", + "@rollup/rollup-linux-arm64-musl": "4.63.5", + "@rollup/rollup-linux-loong64-gnu": "4.63.5", + "@rollup/rollup-linux-loong64-musl": "4.63.5", + "@rollup/rollup-linux-ppc64-gnu": "4.63.5", + "@rollup/rollup-linux-ppc64-musl": "4.63.5", + "@rollup/rollup-linux-riscv64-gnu": "4.63.5", + "@rollup/rollup-linux-riscv64-musl": "4.63.5", + "@rollup/rollup-linux-s390x-gnu": "4.63.5", + "@rollup/rollup-linux-x64-gnu": "4.63.5", + "@rollup/rollup-linux-x64-musl": "4.63.5", + "@rollup/rollup-openbsd-x64": "4.63.5", + "@rollup/rollup-openharmony-arm64": "4.63.5", + "@rollup/rollup-win32-arm64-msvc": "4.63.5", + "@rollup/rollup-win32-ia32-msvc": "4.63.5", + "@rollup/rollup-win32-x64-gnu": "4.63.5", + "@rollup/rollup-win32-x64-msvc": "4.63.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/RaceTimer_2026/racetimer_react/package.json b/RaceTimer_2026/racetimer_react/package.json new file mode 100644 index 0000000..c773cb8 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/package.json @@ -0,0 +1,25 @@ +{ + "name": "racetimer_react", + "private": true, + "version": "2.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "typecheck": "tsc -b" + }, + "dependencies": { + "@tanstack/react-query": "^5.59.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.27.0" + }, + "devDependencies": { + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "^5.6.3", + "vite": "^5.4.10" + } +} diff --git a/RaceTimer_2026/racetimer_react/public/favicon.ico b/RaceTimer_2026/racetimer_react/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a11777cc471a4344702741ab1c8a588998b1311a GIT binary patch literal 3870 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 baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ literal 0 HcmV?d00001 diff --git a/RaceTimer_2026/racetimer_react/src/App.tsx b/RaceTimer_2026/racetimer_react/src/App.tsx new file mode 100644 index 0000000..ce32e51 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/App.tsx @@ -0,0 +1,36 @@ +import { useState } from "react"; +import { Navigate, Route, Routes, useParams } from "react-router-dom"; +import { Layout } from "./components/Layout"; +import { LeaderboardPage } from "./pages/LeaderboardPage"; +import { PlayerPage } from "./pages/PlayerPage"; +import { MapsPage } from "./pages/MapsPage"; +import { MapPage } from "./pages/MapPage"; +import { SearchPage } from "./pages/SearchPage"; +import { PointsPage } from "./pages/PointsPage"; +import { NotFound } from "./components/States"; + +/** Old links looked like #/map//. */ +function OldMapLink() { + const { mapname, stage } = useParams(); + return ; +} + +export function App({ loginError }: { loginError: string | null }) { + const [notice, setNotice] = useState(loginError); + return ( + setNotice(null)}> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); +} diff --git a/RaceTimer_2026/racetimer_react/src/api/client.ts b/RaceTimer_2026/racetimer_react/src/api/client.ts new file mode 100644 index 0000000..665dfd4 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/api/client.ts @@ -0,0 +1,96 @@ +import type { + AuthUser, + CategoryBoard, + HistoryEntry, + MapInfo, + Player, + PlayerMapRow, + ServerTag, +} from "./types"; + +export const API_BASE: string = ( + import.meta.env.VITE_API_BASE ?? "https://racebackend.unloze.com/racetimer_endpoints-1.0/api" +).replace(/\/+$/, ""); + +const TOKEN_KEY = "racetimer.token"; + +export function getToken(): string | null { + try { + return localStorage.getItem(TOKEN_KEY); + } catch { + return null; + } +} + +export function setToken(token: string | null): void { + try { + if (token) localStorage.setItem(TOKEN_KEY, token); + else localStorage.removeItem(TOKEN_KEY); + } catch { + /* storage blocked: the session only lasts for this page load */ + } +} + +export class ApiError extends Error { + constructor(public status: number, message: string) { + super(message); + } +} + +async function request(path: string, init?: RequestInit): Promise { + const headers: Record = { Accept: "application/json" }; + const token = getToken(); + if (token) headers.Authorization = `Bearer ${token}`; + if (init?.body) headers["Content-Type"] = "application/json"; + let res: Response; + try { + res = await fetch(`${API_BASE}/${path}`, { ...init, headers: { ...headers, ...(init?.headers ?? {}) } }); + } catch { + throw new ApiError(0, "Could not reach the racetimer server. Check your connection and try again."); + } + const text = await res.text(); + if (!res.ok) { + let message = `Request failed (${res.status})`; + try { + const body = JSON.parse(text); + if (body?.errorMessage) message = body.errorMessage; + } catch { + /* not JSON */ + } + throw new ApiError(res.status, message); + } + return (text ? JSON.parse(text) : null) as T; +} + +const enc = encodeURIComponent; + +export const api = { + leaderboard: (server: ServerTag, offset: number) => + request(`timers/leaderboard/${offset}?server=${server}`), + player: (steamId: string, server: ServerTag) => + request(`timers/player/${enc(steamId)}?server=${server}`), + playerMaps: (steamId: string, offset: number) => + request(`timers/player/maps/${enc(steamId)}/${offset}`), + playerHistory: (steamId: string, offset: number, categoryId?: number) => + request( + `timers/player/history/${enc(steamId)}/${offset}` + (categoryId ? `?categoryId=${categoryId}` : ""), + ), + allMaps: () => request("timers/allmaps"), + map: (mapName: string) => request(`timers/map/${enc(mapName)}`), + category: (id: number, offset: number) => request(`timers/category/${id}/${offset}`), + searchPlayers: (q: string, server: ServerTag) => + request(`timers/searchplayers/${enc(q)}?server=${server}`), + me: () => request("auth/me"), + setRecordInvalid: (recordId: number, invalid: boolean) => + request<{ id: number; invalid: boolean }>(`admin/records/${recordId}`, { + method: "PUT", + body: JSON.stringify({ invalid }), + }), + setCategoryInvalid: (categoryId: number, invalid: boolean) => + request<{ id: number; invalid: boolean }>(`admin/categories/${categoryId}`, { + method: "PUT", + body: JSON.stringify({ invalid }), + }), +}; + +export const steamLoginUrl = `${API_BASE}/auth/steam/login`; diff --git a/RaceTimer_2026/racetimer_react/src/api/types.ts b/RaceTimer_2026/racetimer_react/src/api/types.ts new file mode 100644 index 0000000..c0ab903 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/api/types.ts @@ -0,0 +1,182 @@ +// JSON shapes returned by the racetimer backend (racetimer.dto.Dto). +// Field names must match the backend exactly. + +export type ServerTag = "ze1" | "ze2"; +export const SERVERS: ServerTag[] = ["ze1", "ze2"]; + +export interface ServerStats { + points: number; + rank: number; + times: number; +} + +export interface Badge { + name: string; + url: string; +} + +export interface Player { + steamID: string; + steamID64: string | null; + name: string; + Avatar: string; + /** Rank on `server`. */ + Rank: number; + /** Points on `server`. */ + PlayerPoints: number; + Times: number; + UrlBanners: string[]; + server: ServerTag; + servers: Record; + badges: Badge[]; +} + +export interface Cvar { + name: string; + value: string; +} + +/** Who marked something invalid and when. Fields are null for rows flagged before this was recorded. */ +export interface Invalidation { + steamID: string | null; + steamID64: string | null; + name: string | null; + /** Unix seconds. */ + at: number | null; +} + +export interface Category { + id: number; + /** "Category N", same numbering as in-game. */ + categoryNumber: number; + mapName: string; + stage: number; + serverTag: ServerTag; + serverCvars: string; + cvars: Cvar[]; + /** Migrated CLASSIC RACETIMER data: points divided by 10, dates unknown. */ + isLegacy: boolean; + isInvalid: boolean; + invalidation?: Invalidation | null; + givesPoints: boolean; + completions: number; + fastestTime: number | null; +} + +export interface Stage { + stage: number; + allCategoriesInvalid: boolean; + categories: Category[]; +} + +export interface MapInfo { + mapName: string; + allCategoriesInvalid: boolean; + stages: Stage[]; +} + +export interface BoardEntry { + recordId: number; + /** 0 for an invalidated run. */ + position: number; + steamID: string; + steamID64: string | null; + name: string; + avatar: string; + badgesUrls: string[]; + time: number; + points: number; + /** 1.0 = no bonus. */ + bonusMultiplier: number; + /** Raised by the small-Classic-board rules (at least 1, top 10 places 1 apart). */ + pointsRaised?: boolean; + /** Unix seconds; null for legacy records. */ + recordedAt: number | null; + isLegacy: boolean; + /** Invalidated by an admin: listed in time order, but no position and no points. */ + isInvalid: boolean; + invalidation?: Invalidation | null; +} + +export interface InvalidatedEntry { + recordId: number; + steamID: string; + steamID64: string | null; + name: string; + avatar: string; + time: number; + recordedAt: number | null; + isLegacy: boolean; + isInvalid: true; + invalidation?: Invalidation | null; +} + +export interface CategoryBoard { + category: Category; + offset: number; + pageSize: number; + /** Valid entries and invalidated runs together, fastest first. */ + entries: BoardEntry[]; + /** The invalidated runs only (they are also in entries). */ + invalidated: InvalidatedEntry[]; +} + +export interface PlayerMapRow { + recordId: number; + categoryId: number; + mapName: string; + stage: number; + categoryNumber: number; + serverTag: ServerTag; + isLegacy: boolean; + categoryInvalid: boolean; + /** This run was invalidated by an admin: position 0, no points. */ + isInvalid: boolean; + invalidation?: Invalidation | null; + categoryInvalidation?: Invalidation | null; + time: number; + position: number; + completions: number; + bonusMultiplier: number; + points: number; + pointsRaised?: boolean; + recordedAt: number | null; +} + +export interface HistoryEntry { + recordId: number; + categoryId: number; + mapName: string; + stage: number; + categoryNumber: number; + serverTag: ServerTag; + isLegacy: boolean; + categoryInvalid: boolean; + time: number; + previousTime: number | null; + improvedBy: number | null; + recordedAt: number | null; + isInvalid: boolean; + invalidation?: Invalidation | null; + categoryInvalidation?: Invalidation | null; + isCurrentBest: boolean; +} + +export interface AuthUser { + steamID: string; + steamID64: string | null; + name: string; + role: "admin" | "staff"; + expiresAt: number; +} + +export interface ApiErrorBody { + statusCode: number; + errorMessage: string; +} + +// Page sizes used by the backend. +export const LEADERBOARD_PAGE = 100; +export const CATEGORY_PAGE = 75; +export const PLAYER_MAPS_PAGE = 50; +export const HISTORY_PAGE = 50; diff --git a/RaceTimer_2026/racetimer_react/src/auth.tsx b/RaceTimer_2026/racetimer_react/src/auth.tsx new file mode 100644 index 0000000..5f21301 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/auth.tsx @@ -0,0 +1,69 @@ +import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react"; +import { api, ApiError, getToken, setToken } from "./api/client"; +import type { AuthUser } from "./api/types"; + +interface AuthState { + user: AuthUser | null; + signOut: () => void; + /** Called when an admin request comes back 401 (session expired). */ + expired: () => void; +} + +const AuthContext = createContext({ user: null, signOut: () => {}, expired: () => {} }); + +const LOGIN_ERRORS: Record = { + not_admin: "That Steam account is not a SourceBans admin, so it cannot sign in here.", + steam_verification_failed: "Steam sign-in could not be verified. Please try again.", + admin_check_unavailable: "The admin check is unavailable right now. Please try again later.", +}; + +/** + * After Steam sign-in the backend sends the browser to "/#token=..." + * or "/#loginError=...". Pick that up before the router sees the hash, + * then clean the address bar. + */ +export function captureLoginRedirect(): string | null { + const hash = window.location.hash; + const clean = () => window.history.replaceState(null, "", window.location.pathname + window.location.search + "#/"); + if (hash.startsWith("#token=")) { + setToken(decodeURIComponent(hash.slice("#token=".length))); + clean(); + return null; + } + if (hash.startsWith("#loginError=")) { + const code = decodeURIComponent(hash.slice("#loginError=".length)); + clean(); + return LOGIN_ERRORS[code] ?? "Sign-in failed."; + } + return null; +} + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + + useEffect(() => { + if (!getToken()) return; + let cancelled = false; + api + .me() + .then((u) => !cancelled && setUser(u)) + .catch((e) => { + if (e instanceof ApiError && e.status === 401) setToken(null); + }); + return () => { + cancelled = true; + }; + }, []); + + const signOut = useCallback(() => { + setToken(null); + setUser(null); + }, []); + + const value = useMemo(() => ({ user, signOut, expired: signOut }), [user, signOut]); + return {children}; +} + +export function useAuth(): AuthState { + return useContext(AuthContext); +} diff --git a/RaceTimer_2026/racetimer_react/src/components/AdminToggle.tsx b/RaceTimer_2026/racetimer_react/src/components/AdminToggle.tsx new file mode 100644 index 0000000..bc03c6f --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/components/AdminToggle.tsx @@ -0,0 +1,67 @@ +import { useState } from "react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { api, ApiError } from "../api/client"; +import { useAuth } from "../auth"; + +/** + * Admin-only button that marks a record or a whole category invalid (or + * restores it). Asks for confirmation first, then refreshes every cached + * view, because points and positions change everywhere. + */ +export function AdminToggle({ + kind, + id, + invalid, + what, +}: { + kind: "record" | "category"; + id: number; + /** Current state. */ + invalid: boolean; + /** Shown in the confirmation, e.g. "jenz's 12.345 on Category 2". */ + what: string; +}) { + const { user, expired } = useAuth(); + const qc = useQueryClient(); + const [confirming, setConfirming] = useState(false); + + const m = useMutation({ + mutationFn: () => (kind === "record" ? api.setRecordInvalid(id, !invalid) : api.setCategoryInvalid(id, !invalid)), + onSuccess: () => { + setConfirming(false); + qc.invalidateQueries(); + }, + onError: (e) => { + if (e instanceof ApiError && e.status === 401) expired(); + }, + }); + + if (!user) return null; + + const action = invalid ? "Restore" : "Invalidate"; + if (!confirming) { + return ( + + ); + } + return ( + + + {action} {what}? + + + + {m.isError && {(m.error as Error).message}} + + ); +} diff --git a/RaceTimer_2026/racetimer_react/src/components/Bits.tsx b/RaceTimer_2026/racetimer_react/src/components/Bits.tsx new file mode 100644 index 0000000..f64db0f --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/components/Bits.tsx @@ -0,0 +1,188 @@ +import { Link } from "react-router-dom"; +import { SERVERS, type Invalidation, type ServerTag } from "../api/types"; +import { bonusLabel, formatDate, formatNumber, serverLabel, timeAgo } from "../format"; + +/** Hide an image that failed to load instead of showing the browser's broken-image icon. */ +export function hideBroken(e: React.SyntheticEvent) { + e.currentTarget.style.visibility = "hidden"; +} + +export function PlayerCell({ + steamID, + name, + avatar, + badges = [], +}: { + steamID: string; + name: string; + avatar?: string; + badges?: string[]; +}) { + return ( + + {avatar && } + {name} + {badges.length > 0 && ( + + )} + + ); +} + +/** #1-#3 get medal colors; the number is always shown so color is never the only signal. */ +export function Rank({ value }: { value: number }) { + const cls = value === 1 ? "rank gold" : value === 2 ? "rank silver" : value === 3 ? "rank bronze" : "rank"; + return #{value}; +} + +export function ServerSwitch({ value, onChange }: { value: ServerTag; onChange: (s: ServerTag) => void }) { + return ( +
+ {SERVERS.map((s) => ( + + ))} +
+ ); +} + +export function ServerTagChip({ tag }: { tag: string }) { + return {serverLabel(tag)}; +} + +export function LegacyChip() { + return ( + + Classic + + ); +} + +export function InvalidChip({ label = "Invalid" }: { label?: string }) { + return ( + + ⚠ {label} + + ); +} + +/** + * "Invalidated by jenz · 30 Sep 2026, 15:20". Shown to everybody. + * Older rows flagged before the admin was recorded say so instead. + */ +export function InvalidatedBy({ + info, + what = "Invalidated", + className = "", +}: { + info: Invalidation | null | undefined; + what?: string; + className?: string; +}) { + const name = info?.name ?? info?.steamID ?? null; + const who = name ? ( + info?.steamID64 ? ( +
+ {name} + + ) : ( + {name} + ) + ) : ( + an admin + ); + return ( + + {what} by {who} + {info?.at != null ? ( + <> + {" · "} + + + ) : ( + !name && (before this was recorded) + )} + + ); +} + +const RAISED_TITLE = + "Raised so small Classic boards stay fair: every time that earns points gets at least 1, and the top 10 places are at least 1 point apart."; + +/** Points, with a small ↑ when the small-Classic-board rules raised them. */ +export function PointsValue({ points, raised }: { points: number; raised?: boolean }) { + if (!raised) return <>{formatNumber(points)}; + return ( + + {formatNumber(points)} + ↑ + + ); +} + +export function BonusChip({ + multiplier, + position, + completions, +}: { + multiplier: number; + position: number; + completions: number; +}) { + if (multiplier <= 1.0001) return –; + const band = bonusLabel(position, completions); + return ( + + ×{Number(multiplier.toFixed(2))} + {band && {band}} + + ); +} + +export function RecordedAt({ at, legacy }: { at: number | null; legacy: boolean }) { + if (legacy || at === null) { + return ( + + Legacy record + + ); + } + return ( + + ); +} + +export function CategoryName({ number, legacy }: { number: number; legacy: boolean }) { + return ( + + Category {number} + {legacy && } + + ); +} + +export function LoadMore({ + hasMore, + loading, + onClick, +}: { + hasMore: boolean; + loading: boolean; + onClick: () => void; +}) { + if (!hasMore) return null; + return ( +
+ +
+ ); +} diff --git a/RaceTimer_2026/racetimer_react/src/components/Layout.tsx b/RaceTimer_2026/racetimer_react/src/components/Layout.tsx new file mode 100644 index 0000000..4461191 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/components/Layout.tsx @@ -0,0 +1,93 @@ +import { useState, type FormEvent, type ReactNode } from "react"; +import { Link, NavLink, useNavigate } from "react-router-dom"; +import { useAuth } from "../auth"; +import { steamLoginUrl } from "../api/client"; + +export function Layout({ + children, + notice, + onDismissNotice, +}: { + children: ReactNode; + notice: string | null; + onDismissNotice: () => void; +}) { + const { user, signOut } = useAuth(); + const navigate = useNavigate(); + const [q, setQ] = useState(""); + + const submit = (e: FormEvent) => { + e.preventDefault(); + const text = q.trim(); + if (text) navigate(`/search?q=${encodeURIComponent(text)}`); + }; + + return ( +
+
+
+ + + + UNLOZE Racetimer + + + +
+ setQ(e.target.value)} + placeholder="Search players or maps" + aria-label="Search players or maps" + /> +
+
+
+ + {user && ( +
+
+ + Signed in as {user.name} ({user.role}). Admin controls are shown on map boards and + profiles. + + +
+
+ )} + + {notice && ( +
+
+ {notice} + +
+
+ )} + +
{children}
+ + +
+ ); +} diff --git a/RaceTimer_2026/racetimer_react/src/components/States.tsx b/RaceTimer_2026/racetimer_react/src/components/States.tsx new file mode 100644 index 0000000..774545f --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/components/States.tsx @@ -0,0 +1,45 @@ +import { Link } from "react-router-dom"; +import { ApiError } from "../api/client"; + +export function Loading({ label = "Loading…" }: { label?: string }) { + return ( +
+
+ ); +} + +export function ErrorBox({ error, onRetry }: { error: unknown; onRetry?: () => void }) { + const message = error instanceof Error ? error.message : "Something went wrong."; + return ( +
+ {message} + {onRetry && ( + + )} +
+ ); +} + +export function isNotFound(error: unknown): boolean { + return error instanceof ApiError && error.status === 404; +} + +export function Empty({ children }: { children: React.ReactNode }) { + return
{children}
; +} + +export function NotFound({ what = "page" }: { what?: string }) { + return ( +
+

Not found

+

This {what} does not exist.

+ + Back to the leaderboard + +
+ ); +} diff --git a/RaceTimer_2026/racetimer_react/src/components/TimeChart.tsx b/RaceTimer_2026/racetimer_react/src/components/TimeChart.tsx new file mode 100644 index 0000000..8401518 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/components/TimeChart.tsx @@ -0,0 +1,145 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import type { HistoryEntry } from "../api/types"; +import { formatDate, formatTime } from "../format"; + +const H = 220; +const PAD = { top: 16, right: 20, bottom: 30, left: 64 }; + +function niceTicks(min: number, max: number, count = 4): number[] { + const span = max - min || 1; + const raw = span / count; + const pow = Math.pow(10, Math.floor(Math.log10(raw))); + const step = [1, 2, 2.5, 5, 10].map((m) => m * pow).find((s) => s >= raw) ?? raw; + const start = Math.ceil(min / step) * step; + const out: number[] = []; + for (let v = start; v <= max + 1e-9; v += step) out.push(Math.round(v * 1000) / 1000); + return out; +} + +/** + * A player's best time in one category over time (lower is better), drawn + * as a step line: the best time stays flat until the next improvement. + * Invalidated runs and legacy records (no date) are left out. + */ +export function TimeChart({ entries }: { entries: HistoryEntry[] }) { + const points = useMemo( + () => + entries + .filter((e) => !e.isInvalid && e.recordedAt !== null) + .map((e) => ({ t: e.recordedAt as number, v: e.time, e })) + .sort((a, b) => a.t - b.t), + [entries], + ); + const [hover, setHover] = useState(null); + const svgRef = useRef(null); + const wrapRef = useRef(null); + // Draw at the real pixel width so text and lines keep their size on any screen. + const [W, setW] = useState(640); + useEffect(() => { + const el = wrapRef.current; + if (!el) return; + const ro = new ResizeObserver(() => setW(Math.max(280, Math.round(el.clientWidth)))); + ro.observe(el); + return () => ro.disconnect(); + }, []); + + if (points.length < 2) return null; + + const t0 = points[0].t; + const t1 = Math.max(points[points.length - 1].t, t0 + 1); + const vMin = Math.min(...points.map((p) => p.v)); + const vMax = Math.max(...points.map((p) => p.v)); + const pad = (vMax - vMin) * 0.12 || 1; + const yMin = vMin - pad; + const yMax = vMax + pad; + const x = (t: number) => PAD.left + ((t - t0) / (t1 - t0)) * (W - PAD.left - PAD.right); + const y = (v: number) => PAD.top + ((v - yMin) / (yMax - yMin)) * (H - PAD.top - PAD.bottom); + + let d = `M${x(points[0].t)},${y(points[0].v)}`; + for (let i = 1; i < points.length; i++) { + d += ` H${x(points[i].t)} V${y(points[i].v)}`; + } + const ticks = niceTicks(yMin, yMax); + const dateTicks = [t0, (t0 + t1) / 2, t1]; + + const onMove = (ev: React.MouseEvent) => { + const svg = svgRef.current; + if (!svg) return; + const r = svg.getBoundingClientRect(); + const px = ((ev.clientX - r.left) / r.width) * W; + let best = 0; + for (let i = 1; i < points.length; i++) { + if (Math.abs(x(points[i].t) - px) < Math.abs(x(points[best].t) - px)) best = i; + } + setHover(best); + }; + + const h = hover === null ? null : points[hover]; + const tipX = h ? x(h.t) : 0; + // Keep the tooltip inside the chart near the edges. + const tipShift = tipX < 90 ? "0%" : tipX > W - 90 ? "-100%" : "-50%"; + + return ( +
+
Best time over time (faster times are higher)
+
+ setHover(null)} + > + {ticks.map((v) => ( + + + + {formatTime(v)} + + + ))} + {dateTicks.map((t, i) => ( + + {new Date(t * 1000).toLocaleDateString("en-GB", { day: "numeric", month: "short" })} + + ))} + + {h && } + {points.map((p, i) => ( + + ))} + {/* Direct label on the latest point only. */} + + {formatTime(points[points.length - 1].v)} + + + {h && ( +
+ {formatTime(h.v)} + {h.e.improvedBy !== null && −{formatTime(h.e.improvedBy)}} +
+ {formatDate(h.t)} +
+ )} +
+
+ ); +} diff --git a/RaceTimer_2026/racetimer_react/src/format.ts b/RaceTimer_2026/racetimer_react/src/format.ts new file mode 100644 index 0000000..58c60f9 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/format.ts @@ -0,0 +1,74 @@ +const number = new Intl.NumberFormat("en-US"); + +/** 12.345 -> "12.345", 83.2 -> "1:23.200", 3723.5 -> "1:02:03.500". */ +export function formatTime(seconds: number): string { + const ms = Math.round(seconds * 1000); + const h = Math.floor(ms / 3_600_000); + const m = Math.floor((ms % 3_600_000) / 60_000); + const s = Math.floor((ms % 60_000) / 1000); + const rest = String(ms % 1000).padStart(3, "0"); + if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${rest}`; + if (m > 0) return `${m}:${String(s).padStart(2, "0")}.${rest}`; + return `${s}.${rest}`; +} + +/** Signed difference, e.g. "+1.204" or "−0.500". */ +export function formatDelta(seconds: number): string { + const sign = seconds < 0 ? "−" : "+"; + return sign + formatTime(Math.abs(seconds)); +} + +export function formatNumber(n: number): string { + return number.format(n); +} + +const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); +const UNITS: [Intl.RelativeTimeFormatUnit, number][] = [ + ["year", 31_536_000], + ["month", 2_592_000], + ["week", 604_800], + ["day", 86_400], + ["hour", 3_600], + ["minute", 60], +]; + +/** Unix seconds -> "3 weeks ago". */ +export function timeAgo(unixSeconds: number, now = Date.now() / 1000): string { + // Never show "in 3 minutes" when a server clock is slightly ahead. + const diff = Math.min(0, unixSeconds - now); + for (const [unit, size] of UNITS) { + if (Math.abs(diff) >= size) return rtf.format(Math.round(diff / size), unit); + } + return "just now"; +} + +/** Unix seconds -> "27 Sep 2026, 14:03:09" in the viewer's own time zone. */ +export function formatDate(unixSeconds: number): string { + return new Date(unixSeconds * 1000).toLocaleString("en-GB", { + day: "numeric", + month: "short", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }); +} + +export function serverLabel(tag: string): string { + return tag.toUpperCase(); +} + +/** Which bonus band a position is in, for the label next to the multiplier. */ +export function bonusLabel(position: number, completions: number): string | null { + if (completions < 100) return null; + const x = (position - 1) / completions; + if (x < 0.01) return "Top 1%"; + if (x < 0.05) return "Top 5%"; + return null; +} + +/** How many players on a board get points (half from 200 completions on). */ +export function pointedCount(completions: number): number { + return completions < 200 ? completions : Math.floor(completions / 2); +} diff --git a/RaceTimer_2026/racetimer_react/src/main.tsx b/RaceTimer_2026/racetimer_react/src/main.tsx new file mode 100644 index 0000000..c292bd0 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/main.tsx @@ -0,0 +1,37 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { HashRouter } from "react-router-dom"; +import { App } from "./App"; +import { AuthProvider, captureLoginRedirect } from "./auth"; +import { ServerProvider } from "./server"; +import { ApiError } from "./api/client"; +import "./styles.css"; + +const loginError = captureLoginRedirect(); + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // The backend recalculates every 30 minutes, so a few minutes of caching is plenty. + staleTime: 5 * 60_000, + refetchOnWindowFocus: false, + retry: (count, error) => !(error instanceof ApiError && error.status >= 400 && error.status < 500) && count < 2, + }, + }, +}); + +createRoot(document.getElementById("root")!).render( + + + + + {/* Hash URLs (#/player/...) keep the old site's links working. */} + + + + + + + , +); diff --git a/RaceTimer_2026/racetimer_react/src/pages/LeaderboardPage.tsx b/RaceTimer_2026/racetimer_react/src/pages/LeaderboardPage.tsx new file mode 100644 index 0000000..9ba70a9 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/pages/LeaderboardPage.tsx @@ -0,0 +1,68 @@ +import { useInfiniteQuery } from "@tanstack/react-query"; +import { Link } from "react-router-dom"; +import { api } from "../api/client"; +import { LEADERBOARD_PAGE } from "../api/types"; +import { LoadMore, PlayerCell, Rank, ServerSwitch } from "../components/Bits"; +import { Empty, ErrorBox, Loading } from "../components/States"; +import { formatNumber, serverLabel } from "../format"; +import { useServer } from "../server"; + +export function LeaderboardPage() { + const { server, setServer } = useServer(); + const q = useInfiniteQuery({ + queryKey: ["leaderboard", server], + queryFn: ({ pageParam }) => api.leaderboard(server, pageParam), + initialPageParam: 0, + getNextPageParam: (last, all) => (last.length < LEADERBOARD_PAGE ? undefined : all.length * LEADERBOARD_PAGE), + }); + const players = q.data?.pages.flat() ?? []; + + return ( + <> +
+
+

{serverLabel(server)} leaderboard

+

+ Players ranked by racetimer points on {serverLabel(server)}. ZE1 and ZE2 are ranked separately.{" "} + How points work +

+
+ +
+ + {q.isPending && } + {q.isError && q.refetch()} />} + {q.isSuccess && players.length === 0 && No one has a time on {serverLabel(server)} yet.} + + {players.length > 0 && ( +
+ + + + + + + + + + + {players.map((p) => ( + + + + + + + ))} + +
RankPlayerPointsRecords
+ + + + {formatNumber(p.PlayerPoints)}{formatNumber(p.Times)}
+
+ )} + q.fetchNextPage()} /> + + ); +} diff --git a/RaceTimer_2026/racetimer_react/src/pages/MapPage.tsx b/RaceTimer_2026/racetimer_react/src/pages/MapPage.tsx new file mode 100644 index 0000000..999b492 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/pages/MapPage.tsx @@ -0,0 +1,340 @@ +import { Fragment } from "react"; +import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; +import { Link, useParams, useSearchParams } from "react-router-dom"; +import { api } from "../api/client"; +import { CATEGORY_PAGE, type Category, type CategoryBoard, type Stage } from "../api/types"; +import { + BonusChip, + InvalidatedBy, + InvalidChip, + LegacyChip, + LoadMore, + PlayerCell, + PointsValue, + Rank, + RecordedAt, + ServerTagChip, +} from "../components/Bits"; +import { AdminToggle } from "../components/AdminToggle"; +import { Empty, ErrorBox, isNotFound, Loading, NotFound } from "../components/States"; +import { formatDelta, formatNumber, formatTime, pointedCount, serverLabel } from "../format"; + +export function MapPage() { + const { mapname = "" } = useParams(); + const [params, setParams] = useSearchParams(); + const map = useQuery({ queryKey: ["map", mapname.toLowerCase()], queryFn: () => api.map(mapname) }); + + if (map.isPending) return ; + if (map.isError) { + return isNotFound(map.error) ? : map.refetch()} />; + } + const m = map.data; + const stageNo = Number(params.get("stage")) || m.stages[0]?.stage; + const stage = m.stages.find((s) => s.stage === stageNo) ?? m.stages[0]; + if (!stage) return This map has no categories yet.; + const catId = Number(params.get("category")); + const category = + stage.categories.find((c) => c.id === catId) ?? + stage.categories.find((c) => !c.isInvalid) ?? + stage.categories[0]; + + const select = (s: number, c?: number) => { + const next = new URLSearchParams(); + next.set("stage", String(s)); + if (c) next.set("category", String(c)); + setParams(next); + }; + + return ( + <> +
+
+

{m.mapName}

+

+ {m.stages.length === 1 ? "1 stage" : `${m.stages.length} stages`} + {m.allCategoriesInvalid && ( + <> + {" "} + · + + )} +

+
+
+ + {m.stages.length > 1 && ( +
+ {m.stages.map((s) => ( + + ))} +
+ )} + + {stage.allCategoriesInvalid && ( +
+ Every category of stage {stage.stage} has been marked invalid by an admin, so this stage gives no points. +
+ )} + + select(stage.stage, c)} /> + + + + ); +} + +function CategoryPicker({ + stage, + selected, + onSelect, +}: { + stage: Stage; + selected: Category; + onSelect: (id: number) => void; +}) { + if (stage.categories.length <= 1) return null; + return ( +
+ {stage.categories.map((c) => ( + + ))} +
+ ); +} + +/** Server settings of the selected category; values that differ between this stage's categories are highlighted. */ +function CategoryDetails({ stage, category }: { stage: Stage; category: Category }) { + const others = stage.categories.filter((c) => c.id !== category.id && !c.isLegacy); + const differs = (name: string, value: string) => + others.some((o) => { + const v = o.cvars.find((x) => x.name === name); + return v !== undefined && v.value !== value; + }); + + return ( +
+
+

+ Category {category.categoryNumber} + {category.isLegacy && } + {category.isInvalid && } +

+ +
+ + {category.isInvalid && ( +
+ + + + + . Its times are still listed, but none of them give points. + +
+ )} + + {category.isLegacy ? ( +

+ Times from the old racetimer (CLASSIC RACETIMER). The server settings were not recorded back then, the exact dates are + unknown, and points from this category are divided by 10. +

+ ) : category.cvars.length > 0 ? ( + <> +

+ Server settings when these times were set. + {others.length > 0 && " Highlighted values differ from another category of this stage."} +

+
+ {category.cvars.map((c) => { + const diff = differs(c.name, c.value); + return ( +
+
{c.name}
+
+ {c.value} + {diff && (differs)} +
+
+ ); + })} +
+ + ) : ( +

{category.serverCvars}

+ )} +
+ ); +} + +function Board({ category }: { category: Category }) { + const q = useInfiniteQuery({ + queryKey: ["category", category.id], + queryFn: ({ pageParam }) => api.category(category.id, pageParam), + initialPageParam: 0, + getNextPageParam: (last: CategoryBoard, all) => + last.entries.length < CATEGORY_PAGE ? undefined : all.length * CATEGORY_PAGE, + }); + const pages = q.data?.pages ?? []; + const entries = pages.flatMap((p) => p.entries); + const hasInvalid = entries.some((e) => e.isInvalid); + const hasRaised = entries.some((e) => e.pointsRaised); + const fresh = pages[0]?.category ?? category; + const cut = fresh.completions >= 200 ? pointedCount(fresh.completions) : null; + const best = entries.find((e) => !e.isInvalid)?.time; + + if (q.isPending) return ; + if (q.isError) return q.refetch()} />; + + // Position of the previous valid row, for drawing the points cut-off line. + let lastValidPosition = 0; + + return ( +
+
+

Leaderboard

+ + {formatNumber(fresh.completions)} {fresh.completions === 1 ? "record" : "records"} + {cut !== null && <> · top {formatNumber(cut)} earn points} + {fresh.completions >= 100 && <> · top 5% get a bonus} + +
+ {hasInvalid && ( +

+ Rows marked were invalidated by an admin (shown under the name, with the date). They are listed where + their time would place them, but they have no position and give no points; the player's previous valid time counts + instead. +

+ )} + {hasRaised && ( +

+ ↑ Classic points are divided by 10, which on a small board would give almost + everyone the same points. So every time that earns points gets at least 1, and each of the top 10 places gets at least 1 + point more than the place below it. How points work +

+ )} + {entries.length === 0 ? ( + No times in this category yet. + ) : ( +
+ + + + + + + + + + + + + + {entries.map((e) => { + const showCut = + !e.isInvalid && cut !== null && lastValidPosition > 0 && lastValidPosition <= cut && e.position > cut; + if (!e.isInvalid) lastValidPosition = e.position; + const what = `${e.name}'s ${formatTime(e.time)}`; + return ( + + {showCut && ( + + + + )} + {e.isInvalid ? ( + + + + + + + + + ) : ( + + + + + + + + + + + )} + + ); + })} + +
#PlayerTimeGapBonusPointsSet +
Points cut-off: only the faster half of this board earns points
+ – + +
+ + +
+ +
{formatTime(e.time)} + + 0 + + + +
+ + + + {formatTime(e.time)} + {best !== undefined && e.time > best ? formatDelta(e.time - best) : ""} + + + + + + + + +
+
+ )} + q.fetchNextPage()} /> +

+ Showing {serverLabel(fresh.serverTag)} times for {fresh.mapName}, stage {fresh.stage}, category {fresh.categoryNumber}. +

+
+ ); +} diff --git a/RaceTimer_2026/racetimer_react/src/pages/MapsPage.tsx b/RaceTimer_2026/racetimer_react/src/pages/MapsPage.tsx new file mode 100644 index 0000000..c3373e7 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/pages/MapsPage.tsx @@ -0,0 +1,92 @@ +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { Link } from "react-router-dom"; +import { api } from "../api/client"; +import type { MapInfo } from "../api/types"; +import { InvalidChip, ServerTagChip } from "../components/Bits"; +import { Empty, ErrorBox, Loading } from "../components/States"; +import { formatNumber } from "../format"; + +export function mapSummary(m: MapInfo) { + const cats = m.stages.flatMap((s) => s.categories); + const servers = Array.from(new Set(cats.map((c) => c.serverTag))).sort(); + const completions = cats.reduce((sum, c) => sum + c.completions, 0); + return { categories: cats.length, servers, completions }; +} + +export function MapsTable({ maps }: { maps: MapInfo[] }) { + return ( +
+ + + + + + + + + + + + {maps.map((m) => { + const s = mapSummary(m); + return ( + + + + + + + + ); + })} + +
MapStagesCategoriesServersRecords
+ + {m.mapName} + + {m.allCategoriesInvalid && ( + <> + {" "} + + + )} + {m.stages.length}{s.categories} + + {s.servers.map((t) => ( + + ))} + + {formatNumber(s.completions)}
+
+ ); +} + +export function MapsPage() { + const q = useQuery({ queryKey: ["allmaps"], queryFn: api.allMaps }); + const [text, setText] = useState(""); + const maps = (q.data ?? []).filter((m) => m.mapName.toLowerCase().includes(text.toLowerCase())); + + return ( + <> +
+
+

Maps

+

Every map with a racetimer. Open a map to see its stages, categories and leaderboards.

+
+ setText(e.target.value)} + aria-label="Filter maps" + /> +
+ {q.isPending && } + {q.isError && q.refetch()} />} + {q.isSuccess && maps.length === 0 && No maps match “{text}”.} + {maps.length > 0 && } + + ); +} diff --git a/RaceTimer_2026/racetimer_react/src/pages/PlayerPage.tsx b/RaceTimer_2026/racetimer_react/src/pages/PlayerPage.tsx new file mode 100644 index 0000000..6e82781 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/pages/PlayerPage.tsx @@ -0,0 +1,349 @@ +import { useMemo, useState } from "react"; +import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; +import { Link, useParams, useSearchParams } from "react-router-dom"; +import { api } from "../api/client"; +import { HISTORY_PAGE, PLAYER_MAPS_PAGE, SERVERS, type HistoryEntry, type Player, type PlayerMapRow } from "../api/types"; +import { + hideBroken, + BonusChip, + CategoryName, + InvalidatedBy, + InvalidChip, + PointsValue, + LoadMore, + RecordedAt, + ServerTagChip, +} from "../components/Bits"; +import { TimeChart } from "../components/TimeChart"; +import { AdminToggle } from "../components/AdminToggle"; +import { Empty, ErrorBox, isNotFound, Loading, NotFound } from "../components/States"; +import { formatNumber, formatTime, serverLabel } from "../format"; +import { useServer } from "../server"; + +type Tab = "records" | "history"; + +export function PlayerPage() { + const { steamid = "" } = useParams(); + const { server } = useServer(); + const [params, setParams] = useSearchParams(); + const tab: Tab = params.get("tab") === "history" ? "history" : "records"; + const categoryId = Number(params.get("category")) || undefined; + + const player = useQuery({ queryKey: ["player", steamid, server], queryFn: () => api.player(steamid, server) }); + + if (player.isPending) return ; + if (player.isError) { + return isNotFound(player.error) ? ( + + ) : ( + player.refetch()} /> + ); + } + const p = player.data; + + const setTab = (t: Tab, category?: number) => { + const next = new URLSearchParams(); + if (t === "history") next.set("tab", "history"); + if (category) next.set("category", String(category)); + setParams(next, { replace: false }); + }; + + return ( + <> + +
+ + +
+ {tab === "records" ? ( + setTab("history", id)} /> + ) : ( + setTab("history", id)} /> + )} + + ); +} + +function ProfileHeader({ p }: { p: Player }) { + const { server } = useServer(); + return ( +
+ +
+

{p.name}

+
+ {p.steamID} + {p.steamID64 && ( + + Steam profile ↗ + + )} +
+ {p.badges.length > 0 && ( +
+ {p.badges.map((b) => ( + + + {b.name} + + ))} +
+ )} +
+
+ {SERVERS.map((s) => { + const st = p.servers[s]; + return ( +
+
{serverLabel(s)} points
+
{formatNumber(st.points)}
+
+ {st.times > 0 ? ( + <> + Rank #{formatNumber(st.rank)} · {formatNumber(st.times)} records + + ) : ( + "No records yet" + )} +
+
+ ); + })} +
+
+ ); +} + +function RecordsTab({ steamid, onShowHistory }: { steamid: string; onShowHistory: (categoryId: number) => void }) { + const [serverFilter, setServerFilter] = useState<"all" | "ze1" | "ze2">("all"); + const [text, setText] = useState(""); + const q = useInfiniteQuery({ + queryKey: ["playerMaps", steamid], + queryFn: ({ pageParam }) => api.playerMaps(steamid, pageParam), + initialPageParam: 0, + getNextPageParam: (last, all) => (last.length < PLAYER_MAPS_PAGE ? undefined : all.length * PLAYER_MAPS_PAGE), + }); + const rows: PlayerMapRow[] = q.data?.pages.flat() ?? []; + const shown = rows.filter( + (r) => + (serverFilter === "all" || r.serverTag === serverFilter) && + (text === "" || r.mapName.toLowerCase().includes(text.toLowerCase())), + ); + + if (q.isPending) return ; + if (q.isError) return q.refetch()} />; + if (rows.length === 0) return This player has no records yet.; + + return ( + <> +
+
+ {(["all", "ze1", "ze2"] as const).map((s) => ( + + ))} +
+ setText(e.target.value)} + aria-label="Filter maps" + /> +
+
+ + + + + + + + + + + + + {shown.map((r) => ( + + + + + + + + + + ))} + +
MapTimePlaceBonusPointsSet +
+ + {r.mapName} + +
+ + Stage {r.stage} · + + + {r.isInvalid && } + {r.categoryInvalid && } +
+ {r.isInvalid && } + {r.categoryInvalid && ( + + )} +
{formatTime(r.time)} + {r.isInvalid ? ( + + – + + ) : ( + <> + #{formatNumber(r.position)} + / {formatNumber(r.completions)} + + )} + + {!r.isInvalid && ( + + )} + + + + + +
+ {!r.isLegacy && ( + + )} + +
+
+
+ {shown.length === 0 && No records match the filter.} + q.fetchNextPage()} /> + + ); +} + +function HistoryTab({ + steamid, + categoryId, + onCategory, +}: { + steamid: string; + categoryId?: number; + onCategory: (id?: number) => void; +}) { + const maps = useQuery({ + queryKey: ["playerMapsAll", steamid], + // Every category the player has a time in, for the filter drop-down. + queryFn: async () => { + const out: PlayerMapRow[] = []; + for (let offset = 0; ; offset += PLAYER_MAPS_PAGE) { + const page = await api.playerMaps(steamid, offset); + out.push(...page); + if (page.length < PLAYER_MAPS_PAGE) return out; + } + }, + }); + const q = useInfiniteQuery({ + queryKey: ["history", steamid, categoryId ?? 0], + queryFn: ({ pageParam }) => api.playerHistory(steamid, pageParam, categoryId), + initialPageParam: 0, + getNextPageParam: (last, all) => (last.length < HISTORY_PAGE ? undefined : all.length * HISTORY_PAGE), + }); + const entries: HistoryEntry[] = q.data?.pages.flat() ?? []; + + const options = useMemo( + () => + (maps.data ?? []) + .filter((r) => !r.isLegacy) + .sort((a, b) => a.mapName.localeCompare(b.mapName) || a.stage - b.stage || a.categoryNumber - b.categoryNumber), + [maps.data], + ); + + return ( + <> +
+ +
+

+ Every saved run was a new personal best when it was set. Classic (migrated) records have no date and are listed last. +

+ {q.isPending && } + {q.isError && q.refetch()} />} + {q.isSuccess && entries.length === 0 && No improvements to show.} + {categoryId && entries.length > 0 && } + {entries.length > 0 && ( +
    + {entries.map((e) => ( +
  1. +
    + +
    +
    +
    + {formatTime(e.time)} + {e.improvedBy !== null && e.improvedBy > 0 && ( + + {" "} + improved by {formatTime(e.improvedBy)}s + + )} + {e.previousTime === null && !e.isInvalid && first time} + {e.isCurrentBest && Current best} + {e.isInvalid && } +
    +
    + {!categoryId ? ( + + ) : ( + <> + {e.mapName} · Stage {e.stage} · Category {e.categoryNumber} + + )}{" "} + · {serverLabel(e.serverTag)} + {e.previousTime !== null && <> · previous best {formatTime(e.previousTime)}} +
    + {e.isInvalid && } + {e.categoryInvalid && ( + + )} +
    +
    + +
    +
  2. + ))} +
+ )} + q.fetchNextPage()} /> + + ); +} diff --git a/RaceTimer_2026/racetimer_react/src/pages/PointsPage.tsx b/RaceTimer_2026/racetimer_react/src/pages/PointsPage.tsx new file mode 100644 index 0000000..6602272 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/pages/PointsPage.tsx @@ -0,0 +1,67 @@ +export function PointsPage() { + return ( +
+

How points work

+

+ Every map stage can have several categories: one per set of server settings (gravity, air + acceleration, tickrate and so on) and per server. Each category has its own leaderboard, and each gives points on its + own. +

+ +

Points per category

+
    +
  • Only your best valid time in a category counts. Players with the same time share the same place.
  • +
  • + On a board with n records, first place gets n points, second place n − 1, and so on. +
  • +
  • + Once a board has 200 or more records, only the faster half earns points. With 200 records the top 100 + do, with 210 records the top 105. +
  • +
+ +

Top 5% bonus

+

+ On boards with at least 100 records, the fastest players get a multiplier that rises smoothly the closer they are to + first place: +

+
    +
  • 4× for first place, sliding down to 2× at the top 1%
  • +
  • 2× at the top 1%, sliding down to no bonus at the top 5%
  • +
+

The multiplier is shown next to each time on the leaderboards and on your profile.

+ +

Classic records

+

+ Times from the old racetimer are kept as Classic categories. Their points are divided by 10, and they + show as “Legacy record” because the exact date was not saved back then. +

+

+ On small Classic boards, dividing by 10 would give almost everyone the same few points. So two extra rules apply: +

+
    +
  • + Every time that earns points gets at least 1 point. +
  • +
  • + Each of the top 10 places gets at least 1 point more than the place below it. +
  • +
+

+ For example, a Classic board with 9 records gives 9, 8, 7 … 1 points instead of 1, 1, 1, 1, 1, 0, 0, 0, 0. On big + Classic boards the top places are already far apart, so nothing changes there. Points that were raised by these rules are + marked with ↑ on the leaderboard. +

+ +

ZE1 and ZE2

+

Points and ranks are counted separately for each server. Use the ZE1 / ZE2 switch on the leaderboard.

+ +

Invalid records

+

+ Admins can mark a single run or a whole category as invalid, for example if it was cheated or the map was broken. An + invalid run gives no points and your previous valid time counts again. An invalid category stays visible but gives no + points to anyone. +

+
+ ); +} diff --git a/RaceTimer_2026/racetimer_react/src/pages/SearchPage.tsx b/RaceTimer_2026/racetimer_react/src/pages/SearchPage.tsx new file mode 100644 index 0000000..97d71db --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/pages/SearchPage.tsx @@ -0,0 +1,70 @@ +import { useQuery } from "@tanstack/react-query"; +import { useSearchParams } from "react-router-dom"; +import { api } from "../api/client"; +import { PlayerCell, Rank } from "../components/Bits"; +import { Empty, ErrorBox, Loading } from "../components/States"; +import { formatNumber, serverLabel } from "../format"; +import { useServer } from "../server"; +import { MapsTable } from "./MapsPage"; + +export function SearchPage() { + const [params] = useSearchParams(); + const q = (params.get("q") ?? "").trim(); + const { server } = useServer(); + const players = useQuery({ + queryKey: ["searchPlayers", q, server], + queryFn: () => api.searchPlayers(q, server), + enabled: q.length > 0, + }); + const maps = useQuery({ queryKey: ["allmaps"], queryFn: api.allMaps, enabled: q.length > 0 }); + const mapHits = (maps.data ?? []).filter((m) => m.mapName.toLowerCase().includes(q.toLowerCase())); + + if (!q) return Type a player name, Steam ID or map name in the search box.; + + return ( + <> +
+
+

Search: “{q}”

+
+
+ +

Players

+ {players.isPending && } + {players.isError && players.refetch()} />} + {players.isSuccess && players.data.length === 0 && No players found.} + {players.isSuccess && players.data.length > 0 && ( +
+ + + + + + + + + + + {players.data.map((p) => ( + + + + + + + ))} + +
Player{serverLabel(server)} rank{serverLabel(server)} pointsSteam ID
+ + {p.Rank > 0 ? : –}{formatNumber(p.PlayerPoints)}{p.steamID}
+
+ )} + +

Maps

+ {maps.isPending && } + {maps.isError && maps.refetch()} />} + {maps.isSuccess && mapHits.length === 0 && No maps found.} + {mapHits.length > 0 && } + + ); +} diff --git a/RaceTimer_2026/racetimer_react/src/server.tsx b/RaceTimer_2026/racetimer_react/src/server.tsx new file mode 100644 index 0000000..df4ea26 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/server.tsx @@ -0,0 +1,39 @@ +import { createContext, useContext, useState, type ReactNode } from "react"; +import { SERVERS, type ServerTag } from "./api/types"; + +const KEY = "racetimer.server"; + +function initial(): ServerTag { + try { + const v = localStorage.getItem(KEY); + if (v && (SERVERS as string[]).includes(v)) return v as ServerTag; + } catch { + /* ignore */ + } + return "ze1"; +} + +interface ServerState { + server: ServerTag; + setServer: (s: ServerTag) => void; +} + +const ServerContext = createContext({ server: "ze1", setServer: () => {} }); + +/** Which server's points (ZE1 or ZE2) the leaderboard and profiles show. Remembered per browser. */ +export function ServerProvider({ children }: { children: ReactNode }) { + const [server, setState] = useState(initial); + const setServer = (s: ServerTag) => { + setState(s); + try { + localStorage.setItem(KEY, s); + } catch { + /* ignore */ + } + }; + return {children}; +} + +export function useServer(): ServerState { + return useContext(ServerContext); +} diff --git a/RaceTimer_2026/racetimer_react/src/styles.css b/RaceTimer_2026/racetimer_react/src/styles.css new file mode 100644 index 0000000..3f44c6a --- /dev/null +++ b/RaceTimer_2026/racetimer_react/src/styles.css @@ -0,0 +1,1043 @@ +/* ---------------------------------------------------------------- tokens */ +:root { + color-scheme: dark; + --bg: #0d1117; + --surface: #161b22; + --surface-2: #1c232d; + --surface-3: #242c38; + --border: #2a3340; + --text: #e6edf3; + --text-2: #b1bac4; + --muted: #8b96a3; + --accent: #ff5c62; + --accent-soft: rgba(255, 92, 98, 0.14); + --link: #6cb6ff; + --good: #3fb950; + --warn: #d29922; + --warn-soft: rgba(210, 153, 34, 0.12); + --danger: #f85149; + --danger-soft: rgba(248, 81, 73, 0.12); + --gold: #e3b341; + --silver: #c3cad3; + --bronze: #d08a4c; + --ze1: #3987e5; + --ze2: #9085e9; + --chart: #3987e5; + --radius: 10px; + --radius-sm: 6px; + --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans", Ubuntu, sans-serif; + --mono: ui-monospace, SFMono-Regular, "JetBrains Mono", Menlo, Consolas, monospace; +} + +* { + box-sizing: border-box; +} +html, +body { + margin: 0; + background: var(--bg); + color: var(--text); + font: 15px/1.5 var(--font); + -webkit-font-smoothing: antialiased; +} +a { + color: var(--link); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +h1, +h2, +h3 { + line-height: 1.25; + margin: 0 0 0.5rem; + font-weight: 650; +} +h1 { + font-size: 1.75rem; +} +h2 { + font-size: 1.2rem; + margin-top: 1.75rem; +} +h3 { + font-size: 1rem; +} +code, +.mono { + font-family: var(--mono); + font-variant-numeric: tabular-nums; +} +:focus-visible { + outline: 2px solid var(--link); + outline-offset: 2px; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; +} + +/* ---------------------------------------------------------------- layout */ +.app { + min-height: 100vh; + display: flex; + flex-direction: column; +} +.container { + width: 100%; + max-width: 1180px; + margin: 0 auto; + padding: 0 16px; +} +.main { + flex: 1; + padding-top: 24px; + padding-bottom: 48px; +} + +.topbar { + position: sticky; + top: 0; + z-index: 10; + background: rgba(13, 17, 23, 0.92); + backdrop-filter: blur(8px); + border-bottom: 1px solid var(--border); +} +.topbar-inner { + max-width: 1180px; + margin: 0 auto; + padding: 10px 16px; + display: flex; + align-items: center; + gap: 20px; +} +.brand { + display: flex; + align-items: center; + gap: 8px; + color: var(--text); + font-weight: 700; + letter-spacing: 0.02em; + white-space: nowrap; +} +.brand:hover { + text-decoration: none; +} +.brand-mark { + display: grid; + place-items: center; + width: 30px; + height: 30px; + border-radius: 8px; + background: var(--accent-soft); + color: var(--accent); +} +.brand-sub { + color: var(--accent); +} +.mainnav { + display: flex; + gap: 4px; +} +.mainnav a { + color: var(--text-2); + padding: 6px 12px; + border-radius: var(--radius-sm); + font-weight: 500; +} +.mainnav a:hover { + color: var(--text); + background: var(--surface-2); + text-decoration: none; +} +.mainnav a.active { + color: var(--text); + background: var(--surface-3); +} +.search { + margin-left: auto; + flex: 0 1 280px; +} +.search input, +.input { + width: 100%; + background: var(--surface); + border: 1px solid var(--border); + color: var(--text); + border-radius: var(--radius-sm); + padding: 8px 12px; + font: inherit; +} +.search input:focus, +.input:focus { + outline: none; + border-color: var(--link); +} +select.input { + cursor: pointer; +} + +.adminbar { + background: var(--warn-soft); + border-bottom: 1px solid rgba(210, 153, 34, 0.35); + font-size: 0.9rem; +} +.adminbar-inner { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + padding-top: 6px; + padding-bottom: 6px; +} + +.footer { + border-top: 1px solid var(--border); + color: var(--muted); + font-size: 0.875rem; +} +.footer-inner { + display: flex; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + padding-top: 18px; + padding-bottom: 18px; +} +.footer-admin { + color: var(--muted); +} + +.page-head { + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 16px; + flex-wrap: wrap; + margin-bottom: 20px; +} +.page-head .input { + max-width: 280px; +} +.lead { + color: var(--text-2); + margin: 0; + max-width: 720px; +} +.hint { + color: var(--muted); + font-size: 0.875rem; + margin: 8px 0 12px; +} +.muted { + color: var(--muted); +} +.strong { + font-weight: 650; +} +.good { + color: var(--good); +} +.error-text { + color: var(--danger); + font-size: 0.85rem; +} +.sub { + color: var(--muted); + font-size: 0.8rem; +} + +/* ---------------------------------------------------------------- controls */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + background: var(--surface-3); + color: var(--text); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 8px 14px; + font: inherit; + font-weight: 550; + cursor: pointer; + white-space: nowrap; +} +.btn:hover:not(:disabled) { + border-color: #3a4554; + background: #2b3441; + text-decoration: none; +} +.btn:disabled { + opacity: 0.6; + cursor: default; +} +.btn-sm { + padding: 3px 10px; + font-size: 0.8rem; +} +.btn-ghost { + background: transparent; +} +.btn-danger { + background: var(--danger); + border-color: var(--danger); + color: #fff; +} +.btn-danger:hover:not(:disabled) { + background: #ff6a61; + border-color: #ff6a61; +} +.btn-danger-ghost { + background: transparent; + color: var(--danger); + border-color: rgba(248, 81, 73, 0.4); +} +.btn-danger-ghost:hover:not(:disabled) { + background: var(--danger-soft); +} +.linklike { + background: none; + border: 0; + padding: 0; + color: var(--link); + font: inherit; + cursor: pointer; + text-align: left; +} +.linklike:hover { + text-decoration: underline; +} +.confirm { + display: inline-flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; +} +.confirm-text { + font-size: 0.8rem; + color: var(--text-2); +} + +.segmented { + display: inline-flex; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 3px; +} +.segmented button { + background: none; + border: 0; + color: var(--text-2); + padding: 5px 14px; + border-radius: 6px; + font: inherit; + font-weight: 600; + cursor: pointer; +} +.segmented button.on { + background: var(--surface-3); + color: var(--text); +} + +.tabs { + display: flex; + gap: 2px; + border-bottom: 1px solid var(--border); + margin: 20px 0 16px; + overflow-x: auto; +} +.tabs button { + background: none; + border: 0; + border-bottom: 2px solid transparent; + color: var(--text-2); + padding: 10px 14px; + font: inherit; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + margin-bottom: -1px; +} +.tabs button:hover { + color: var(--text); +} +.tabs button.on { + color: var(--text); + border-bottom-color: var(--accent); +} +.tab-flag { + color: var(--warn); +} + +.toolbar { + display: flex; + gap: 12px; + align-items: flex-end; + flex-wrap: wrap; + margin-bottom: 12px; +} +.toolbar .input { + max-width: 260px; +} +.field { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 0.8rem; + color: var(--muted); + flex: 1 1 360px; +} +.field .input { + max-width: 560px; +} + +.loadmore { + display: flex; + justify-content: center; + margin: 16px 0; +} + +/* ---------------------------------------------------------------- chips */ +.chip { + display: inline-flex; + align-items: center; + gap: 3px; + font-size: 0.72rem; + font-weight: 650; + letter-spacing: 0.02em; + padding: 1px 7px; + border-radius: 999px; + border: 1px solid var(--border); + color: var(--text-2); + background: var(--surface-2); + white-space: nowrap; + vertical-align: middle; +} +.chip-server { + color: var(--text); +} +.chip-ze1 { + border-color: rgba(57, 135, 229, 0.55); + background: rgba(57, 135, 229, 0.14); +} +.chip-ze2 { + border-color: rgba(144, 133, 233, 0.55); + background: rgba(144, 133, 233, 0.14); +} +.chip-legacy { + color: var(--text-2); +} +.chip-invalid { + color: var(--warn); + border-color: rgba(210, 153, 34, 0.45); + background: var(--warn-soft); +} +.chip-bonus { + color: var(--gold); + border-color: rgba(227, 179, 65, 0.45); + background: rgba(227, 179, 65, 0.1); + font-family: var(--mono); +} +.chip-bonus-band { + font-family: var(--font); + color: var(--text-2); + font-weight: 550; +} +.chip-best { + color: var(--good); + border-color: rgba(63, 185, 80, 0.45); + background: rgba(63, 185, 80, 0.1); + margin-left: 8px; +} +.cell-chips { + display: inline-flex; + gap: 4px; + flex-wrap: wrap; +} +.cat-name { + display: inline-flex; + align-items: center; + gap: 6px; + white-space: nowrap; +} +.cell-stack { + display: flex; + flex-direction: column; + gap: 3px; +} + +/* ---------------------------------------------------------------- tables */ +.table-wrap { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow-x: auto; +} +.table { + width: 100%; + border-collapse: collapse; +} +.table th, +.table td { + padding: 9px 12px; + text-align: left; + vertical-align: middle; + border-bottom: 1px solid var(--border); +} +.table th { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--muted); + font-weight: 650; + background: var(--surface-2); + white-space: nowrap; +} +.table tbody tr:last-child td { + border-bottom: 0; +} +.table tbody tr:hover td { + background: rgba(255, 255, 255, 0.02); +} +.table .num { + text-align: right; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} +.row-invalid td { + opacity: 0.72; +} +.run-invalid td { + background: var(--danger-soft); +} +.run-invalid:hover td { + background: var(--danger-soft) !important; +} +.strike { + text-decoration: line-through; + text-decoration-color: var(--danger); +} +.raised-mark { + color: var(--link); + font-weight: 700; +} +time.date { + white-space: nowrap; + font-variant-numeric: tabular-nums; +} +.invalidated-by { + color: var(--text-2); +} +.invalidated-by.small { + display: block; + font-size: 0.78rem; + margin-top: 3px; + font-weight: 400; +} +.invalidated-by a { + color: inherit; + text-decoration: underline; + text-decoration-color: var(--border); +} +.invalidated-by a:hover { + color: var(--text); +} +.cell-with-chip { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} +.row-actions { + display: flex; + gap: 6px; + align-items: center; + justify-content: flex-end; +} +.cut-row td { + background: var(--danger-soft) !important; + color: var(--text-2); + font-size: 0.8rem; + text-align: center; + padding: 5px 12px; +} + +.player-cell { + display: inline-flex; + align-items: center; + gap: 10px; + color: var(--text); + font-weight: 550; + min-width: 0; +} +.player-cell:hover .player-name { + text-decoration: underline; +} +.player-cell:hover { + text-decoration: none; +} +.player-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 260px; +} +.avatar { + width: 28px; + height: 28px; + border-radius: 6px; + background: var(--surface-3); + flex: none; +} +.badges { + display: inline-flex; + gap: 3px; +} +.badges img { + height: 16px; +} +.map-link { + color: var(--text); + font-weight: 550; + overflow-wrap: anywhere; +} +.map-cell { + min-width: 180px; +} +.map-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 4px 6px; + margin-top: 2px; +} +.map-meta .cat-name { + gap: 4px; +} + +.rank { + font-weight: 700; + color: var(--text-2); +} +.rank.gold { + color: var(--gold); +} +.rank.silver { + color: var(--silver); +} +.rank.bronze { + color: var(--bronze); +} + +/* ---------------------------------------------------------------- states */ +.state { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + color: var(--muted); + padding: 40px 16px; + text-align: center; + flex-direction: column; +} +.state.empty { + background: var(--surface); + border: 1px dashed var(--border); + border-radius: var(--radius); + margin: 12px 0; +} +.spinner { + width: 22px; + height: 22px; + border: 3px solid var(--surface-3); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} +.alert { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + padding: 10px 14px; + border-radius: var(--radius-sm); + margin: 12px 0; + border: 1px solid; +} +.alert-danger { + background: var(--danger-soft); + border-color: rgba(248, 81, 73, 0.4); +} +.alert-warn { + background: var(--warn-soft); + border-color: rgba(210, 153, 34, 0.4); +} + +/* ---------------------------------------------------------------- profile */ +.profile { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 20px; + align-items: center; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 20px; +} +.profile-avatar { + width: 96px; + height: 96px; + border-radius: 12px; + background: var(--surface-3); +} +.profile-main h1 { + margin-bottom: 4px; + word-break: break-word; +} +.profile-ids { + display: flex; + gap: 14px; + flex-wrap: wrap; + color: var(--muted); + font-size: 0.875rem; +} +.profile-badges { + display: flex; + gap: 6px; + flex-wrap: wrap; + margin-top: 10px; +} +.badge-pill { + display: inline-flex; + align-items: center; + gap: 6px; + background: var(--surface-2); + border: 1px solid var(--border); + border-radius: 999px; + padding: 2px 10px 2px 4px; + font-size: 0.8rem; +} +.badge-pill img { + height: 18px; +} +.stat-row { + display: flex; + gap: 12px; +} +.stat { + min-width: 160px; + background: var(--surface-2); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 12px 14px; +} +.stat-on { + border-color: rgba(255, 92, 98, 0.5); +} +.stat-label { + font-size: 0.8rem; + color: var(--muted); +} +.stat-value { + font-size: 1.6rem; + font-weight: 650; +} +.stat-sub { + font-size: 0.8rem; + color: var(--text-2); +} + +/* ---------------------------------------------------------------- history */ +.timeline { + list-style: none; + margin: 0; + padding: 0; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); +} +.timeline li { + display: grid; + grid-template-columns: 140px 1fr auto; + gap: 14px; + padding: 12px 16px; + border-bottom: 1px solid var(--border); + align-items: center; +} +.timeline li:last-child { + border-bottom: 0; +} +.tl-when { + color: var(--text-2); + font-size: 0.875rem; +} +.tl-title { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 4px; +} +.tl-invalid { + background: var(--warn-soft); +} +.tl-invalid .tl-title .mono { + text-decoration: line-through; + color: var(--muted); +} + +/* ---------------------------------------------------------------- map page */ +.map-title { + word-break: break-all; +} +.cat-picker { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 10px; + margin-bottom: 16px; +} +.cat-card { + display: flex; + flex-direction: column; + gap: 4px; + text-align: left; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 12px 14px; + color: var(--text); + font: inherit; + cursor: pointer; +} +.cat-card:hover { + border-color: #3a4554; +} +.cat-card.on { + border-color: var(--accent); + box-shadow: 0 0 0 1px var(--accent) inset; +} +.cat-card.is-invalid { + opacity: 0.8; +} +.cat-card-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 650; +} +.cat-card-sub { + color: var(--muted); + font-size: 0.82rem; +} +.panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px 18px; + margin-bottom: 20px; +} +.panel-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} +.panel-head h2 { + margin: 0; + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} +.cvars { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 8px; + margin: 0; +} +.cvar { + display: flex; + justify-content: space-between; + gap: 10px; + background: var(--surface-2); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 6px 10px; + font-family: var(--mono); + font-size: 0.82rem; +} +.cvar dt { + color: var(--text-2); +} +.cvar dd { + margin: 0; + font-weight: 650; +} +.cvar.diff { + border-color: rgba(227, 179, 65, 0.6); + background: rgba(227, 179, 65, 0.1); +} +.cvar.diff dd::after { + content: " ≠"; + color: var(--gold); +} +.board-head { + display: flex; + align-items: baseline; + gap: 12px; + flex-wrap: wrap; + margin: 8px 0 10px; +} +.board-head h2 { + margin: 0; +} +.invalidated { + margin-top: 24px; +} + +/* ---------------------------------------------------------------- chart */ +.chart { + margin: 0 0 16px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 14px 16px 8px; +} +.chart-title { + font-weight: 600; + margin-bottom: 6px; +} +.chart-wrap { + position: relative; +} +.chart svg { + display: block; + width: 100%; + height: auto; +} +.chart-grid { + stroke: var(--border); + stroke-width: 1; +} +.chart-axis { + fill: var(--muted); + font-size: 11px; + font-family: var(--mono); +} +.chart-line { + fill: none; + stroke: var(--chart); + stroke-width: 2; + stroke-linejoin: round; + stroke-linecap: round; +} +.chart-dot { + fill: var(--chart); + stroke: var(--surface); + stroke-width: 2; +} +.chart-crosshair { + stroke: var(--muted); + stroke-width: 1; +} +.chart-label { + fill: var(--text); + font-size: 12px; + font-weight: 650; + font-family: var(--mono); +} +.chart-tip { + position: absolute; + + background: var(--surface-3); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 6px 10px; + font-size: 0.8rem; + pointer-events: none; + white-space: nowrap; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); +} + +/* ---------------------------------------------------------------- prose */ +.prose { + max-width: 760px; +} +.prose p, +.prose li { + color: var(--text-2); +} +.prose strong, +.prose em { + color: var(--text); +} + +/* ---------------------------------------------------------------- small screens */ +@media (max-width: 820px) { + .topbar-inner { + flex-wrap: wrap; + gap: 8px 12px; + } + .search { + order: 3; + flex: 1 1 100%; + } + .profile { + grid-template-columns: auto 1fr; + } + .stat-row { + grid-column: 1 / -1; + } + .stat { + flex: 1; + min-width: 0; + } +} +@media (max-width: 640px) { + h1 { + font-size: 1.4rem; + } + .hide-sm { + display: none; + } + .table th, + .table td { + padding: 8px; + } + .player-name { + max-width: 150px; + } + .profile-avatar { + width: 64px; + height: 64px; + } + .timeline li { + grid-template-columns: 1fr; + gap: 4px; + } + .adminbar-inner { + flex-direction: column; + align-items: flex-start; + } + .page-head .input, + .toolbar .input { + max-width: none; + } +} + +.table time, +.table td > .muted { + white-space: nowrap; +} diff --git a/RaceTimer_2026/racetimer_react/tsconfig.json b/RaceTimer_2026/racetimer_react/tsconfig.json new file mode 100644 index 0000000..0fb7953 --- /dev/null +++ b/RaceTimer_2026/racetimer_react/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "resolveJsonModule": true, + "noEmit": true, + "types": ["vite/client"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/RaceTimer_2026/racetimer_react/vite.config.ts b/RaceTimer_2026/racetimer_react/vite.config.ts new file mode 100644 index 0000000..82ca2af --- /dev/null +++ b/RaceTimer_2026/racetimer_react/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// base "./" lets the built site be served from any folder on the web server. +export default defineConfig({ + base: "./", + plugins: [react()], +}); diff --git a/RaceTimer_2026/racetimer_react_2026_build.zip b/RaceTimer_2026/racetimer_react_2026_build.zip new file mode 100644 index 0000000000000000000000000000000000000000..62038083511ea9cfbd09eb9c4ae5db2570e11a90 GIT binary patch literal 86106 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 N%*8%4wSRB+zW~0^Tu=Z2 literal 0 HcmV?d00001