Add map-specific tournament creation link
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 3m31s
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 3m31s
This commit is contained in:
@@ -2,6 +2,13 @@
|
||||
max-width: 1180px;
|
||||
min-height: 60vh;
|
||||
}
|
||||
.start-panel .tournament-map-action {
|
||||
margin-top: 12px;
|
||||
border-color: var(--green);
|
||||
color: var(--green);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
.tournament-page .eyebrow,
|
||||
.tournament-server-clock,
|
||||
.tournament-card p,
|
||||
|
||||
@@ -2,13 +2,20 @@ import { Header, Footer } from "@/components/Shell";
|
||||
import { TournamentIndex } from "@/components/TournamentLobby";
|
||||
import { user } from "@/lib/supabase";
|
||||
|
||||
export default async function TournamentsPage() {
|
||||
export default async function TournamentsPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ map?: string | string[] }>;
|
||||
}) {
|
||||
const account = await user();
|
||||
const { map } = await searchParams;
|
||||
const initialMapId =
|
||||
typeof map === "string" && /^[0-9a-f-]{36}$/i.test(map) ? map : null;
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main id="main-content">
|
||||
<TournamentIndex signedIn={!!account} />
|
||||
<TournamentIndex signedIn={!!account} initialMapId={initialMapId} />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useI18n } from "@/components/I18nProvider";
|
||||
import { useEffect, useRef, useState, type FormEvent } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowRight, Timer, ZoomIn, Users } from "lucide-react";
|
||||
import { ArrowRight, Timer, ZoomIn, Users, Trophy } from "lucide-react";
|
||||
import {
|
||||
difficulties,
|
||||
customDifficultySchema,
|
||||
@@ -42,6 +42,7 @@ export function StartGame({
|
||||
[loading, setLoading] = useState(false),
|
||||
[error, setError] = useState("");
|
||||
const router = useRouter();
|
||||
const tournamentPath = `/tournaments?map=${encodeURIComponent(datasetId)}`;
|
||||
const starting = useRef(false);
|
||||
const savingPreset = useRef(false);
|
||||
useEffect(() => {
|
||||
@@ -147,6 +148,22 @@ export function StartGame({
|
||||
<Users size={15} /> {t("Multiplayer")}
|
||||
</button>
|
||||
</div>
|
||||
<Link
|
||||
className="button tournament-map-action full"
|
||||
href={
|
||||
hostName
|
||||
? tournamentPath
|
||||
: `/log-in?next=${encodeURIComponent(tournamentPath)}`
|
||||
}
|
||||
>
|
||||
<Trophy size={17} />
|
||||
{t(
|
||||
hostName
|
||||
? "Create tournament with this map"
|
||||
: "Log in to create a tournament",
|
||||
)}
|
||||
<ArrowRight size={17} />
|
||||
</Link>
|
||||
<div className="difficulty-options">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
|
||||
@@ -37,7 +37,13 @@ function time(value: string, zone: string) {
|
||||
timeZone: zone,
|
||||
}).format(new Date(value));
|
||||
}
|
||||
export function TournamentIndex({ signedIn }: { signedIn: boolean }) {
|
||||
export function TournamentIndex({
|
||||
signedIn,
|
||||
initialMapId = null,
|
||||
}: {
|
||||
signedIn: boolean;
|
||||
initialMapId?: string | null;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const [events, setEvents] = useState<Events | null>(null);
|
||||
@@ -45,7 +51,8 @@ export function TournamentIndex({ signedIn }: { signedIn: boolean }) {
|
||||
const [presets, setPresets] = useState<DifficultyPreset[]>([]);
|
||||
const [error, setError] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [creating, setCreating] = useState(!!(signedIn && initialMapId));
|
||||
const [selectedMap, setSelectedMap] = useState(initialMapId ?? "");
|
||||
const [clock, setClock] = useState(0);
|
||||
const [timerMode, setTimerMode] = useState<"shared" | "perMini">("shared");
|
||||
const [roundSeconds, setRoundSeconds] = useState(450);
|
||||
@@ -83,6 +90,9 @@ export function TournamentIndex({ signedIn }: { signedIn: boolean }) {
|
||||
if (active) {
|
||||
setMaps(catalog.maps);
|
||||
setPresets(difficulties.presets);
|
||||
setSelectedMap((current) =>
|
||||
catalog.maps.some((map) => map.id === current) ? current : "",
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
@@ -183,7 +193,12 @@ export function TournamentIndex({ signedIn }: { signedIn: boolean }) {
|
||||
</label>
|
||||
<label>
|
||||
{t("Map")}
|
||||
<select name="dataset" required defaultValue="">
|
||||
<select
|
||||
name="dataset"
|
||||
required
|
||||
value={selectedMap}
|
||||
onChange={(event) => setSelectedMap(event.target.value)}
|
||||
>
|
||||
<option value="" disabled>
|
||||
{t("Choose a map")}
|
||||
</option>
|
||||
|
||||
@@ -79,6 +79,7 @@ export function authDestination(value: string | null | undefined): string {
|
||||
return value;
|
||||
if (value && /^\/dashboard\/[0-9a-f-]{36}$/i.test(value)) return value;
|
||||
if (value && /^\/support\/[0-9a-f-]{36}$/i.test(value)) return value;
|
||||
if (value && /^\/tournaments\?map=[0-9a-f-]{36}$/i.test(value)) return value;
|
||||
if (value && /^\/pair\?code=[A-F0-9]{8}$/.test(value)) return value;
|
||||
return "/dashboard";
|
||||
}
|
||||
|
||||
@@ -971,6 +971,8 @@
|
||||
"One world. Up to 32 rivals.": "Один мир. До 32 соперников.",
|
||||
"Every tournament round contains five mini-rounds. Everyone plays the same locations.": "Каждый раунд турнира состоит из пяти мини-раундов. Все играют в одних и тех же локациях.",
|
||||
"Create tournament": "Создать турнир",
|
||||
"Create tournament with this map": "Создать турнир на этой карте",
|
||||
"Log in to create a tournament": "Войдите, чтобы создать турнир",
|
||||
"Log in to create or join": "Войдите, чтобы создать турнир или участвовать",
|
||||
"Server time": "Время сервера",
|
||||
"Tournament name": "Название турнира",
|
||||
|
||||
@@ -75,6 +75,12 @@ test("authentication redirects accept supported local routes and reject open red
|
||||
assert.equal(authDestination(next), "/dashboard");
|
||||
assert.equal(authDestination("/settings"), "/settings");
|
||||
assert.equal(authDestination("/servers"), "/servers");
|
||||
const tournamentMap = "/tournaments?map=123e4567-e89b-12d3-a456-426614174000";
|
||||
assert.equal(authDestination(tournamentMap), tournamentMap);
|
||||
assert.equal(
|
||||
authDestination(`${tournamentMap}&next=//evil.example`),
|
||||
"/dashboard",
|
||||
);
|
||||
assert.equal(authDestination("/pair?code=1234ABCD"), "/pair?code=1234ABCD");
|
||||
assert.equal(authDestination("/auth/reset-password"), "/auth/reset-password");
|
||||
});
|
||||
|
||||
@@ -23,14 +23,24 @@ let tournamentWarningAt = 0;
|
||||
async function tickTournaments() {
|
||||
if (stopping || tournamentTickRunning) return;
|
||||
tournamentTickRunning = true;
|
||||
let phase = "scheduling";
|
||||
try {
|
||||
check(await client.rpc("tick_tournaments"));
|
||||
const result = await client.rpc("tick_tournaments");
|
||||
if (result.error)
|
||||
throw new Error(
|
||||
`${result.error.code || "RPC error"}: ${result.error.message}`,
|
||||
);
|
||||
phase = "Discord announcements";
|
||||
await dispatchTournamentAnnouncements(client);
|
||||
} catch {
|
||||
if (tournamentWarningAt) {
|
||||
console.info("Tournament scheduling and announcements resumed.");
|
||||
tournamentWarningAt = 0;
|
||||
}
|
||||
} catch (error) {
|
||||
if (Date.now() - tournamentWarningAt >= 300000) {
|
||||
tournamentWarningAt = Date.now();
|
||||
console.error(
|
||||
"Tournament scheduling paused; check database migrations and connectivity. The worker will retry.",
|
||||
`Tournament ${phase} paused: ${summarizeWorkerError(error)}. The worker will retry in 30 seconds.`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user