Improve race setup navigation and manage flow
This commit is contained in:
126
src/app.js
126
src/app.js
@@ -213,7 +213,14 @@ const TRANSLATIONS = {
|
||||
"events.summary_warning_no_finals": "Inga finalsessioner finns ännu.",
|
||||
"events.summary_warning_no_teams": "Endurance/Team Race saknar lag. Lägg upp lag i Setup innan körning.",
|
||||
"events.summary_warning_invalid_lap_window": "Min/max varvtid ser fel ut. Max måste vara större än min.",
|
||||
"events.detail_drivers": "förare",
|
||||
"events.detail_teams": "lag",
|
||||
"events.detail_sessions": "sessioner",
|
||||
"events.detail_results": "resultat",
|
||||
"events.detail_active": "aktiva",
|
||||
"guide.manage_steps_6": "6. Statusbrickorna visar om varje steg är klart, väntar eller behöver åtgärdas. Sammanfattningen till höger visar också varningar om deltagare, lag eller sessioner saknas.",
|
||||
"guide.manage_steps_7": "7. Klicka på en varning i sammanfattningen för att hoppa direkt till rätt sektion i Hantera i stället för att leta manuellt.",
|
||||
"guide.manage_steps_8": "8. Stegkorten högst upp fungerar också som snabbhopp. Klicka på Setup, Format, Generering eller Live / resultat för att gå direkt till rätt block.",
|
||||
"guide.race_wizard_title": "Create Race Wizard",
|
||||
"guide.race_wizard_1": "1. Börja i Race Setup och använd wizarden när du skapar ett nytt race, i stället för att bygga allt direkt i Hantera.",
|
||||
"guide.race_wizard_2": "2. Steg 1 sätter namn, datum, klass och preset. Presetet fyller rimliga standardvärden innan du finjusterar något.",
|
||||
@@ -940,7 +947,14 @@ const TRANSLATIONS = {
|
||||
"events.summary_warning_no_finals": "No finals exist yet.",
|
||||
"events.summary_warning_no_teams": "Endurance/Team Race has no teams yet. Add teams in Setup before running.",
|
||||
"events.summary_warning_invalid_lap_window": "The min/max lap window looks invalid. Max must be greater than min.",
|
||||
"events.detail_drivers": "drivers",
|
||||
"events.detail_teams": "teams",
|
||||
"events.detail_sessions": "sessions",
|
||||
"events.detail_results": "results",
|
||||
"events.detail_active": "active",
|
||||
"guide.manage_steps_6": "6. The status badges show whether each step is complete, pending, or needs action. The summary on the right also warns when participants, teams, or sessions are missing.",
|
||||
"guide.manage_steps_7": "7. Click a warning in the summary to jump straight to the matching Manage section instead of hunting for it manually.",
|
||||
"guide.manage_steps_8": "8. The step cards at the top also work as quick jumps. Click Setup, Format, Generation or Live / results to move straight to the right block.",
|
||||
"guide.race_wizard_title": "Create Race Wizard",
|
||||
"guide.race_wizard_1": "1. Start in Race Setup and use the wizard when creating a new race instead of building everything directly in Manage.",
|
||||
"guide.race_wizard_2": "2. Step 1 sets name, date, class and preset. The preset fills sensible defaults before you fine-tune anything.",
|
||||
@@ -3779,7 +3793,7 @@ function renderEventManager(eventId) {
|
||||
}
|
||||
|
||||
eventManageArea.innerHTML = `
|
||||
<section class="panel">
|
||||
<section class="panel" id="manage-session-plan">
|
||||
<div class="panel-header">
|
||||
<h3>${t("events.manage_title")}: ${escapeHtml(event.name)}</h3>
|
||||
</div>
|
||||
@@ -3926,27 +3940,31 @@ function renderEventManager(eventId) {
|
||||
? `
|
||||
<section class="panel mt-16">
|
||||
<div class="panel-body manage-step-grid">
|
||||
<article class="manage-step-card manage-step-card-${manageStatuses?.setup || "pending"}">
|
||||
<div class="manage-step-head"><strong>${t("events.manage_step_setup")}</strong>${renderManageStatusBadge(manageStatuses?.setup || "pending")}</div>
|
||||
<article class="manage-step-card manage-step-card-${manageStatuses?.setup?.status || "pending"} manage-step-card-link" data-target="manage-session-plan" role="button" tabindex="0">
|
||||
<div class="manage-step-head"><strong>${t("events.manage_step_setup")}</strong>${renderManageStatusBadge(manageStatuses?.setup?.status || "pending")}</div>
|
||||
<div class="manage-step-meta">${escapeHtml(manageStatuses?.setup?.detail || "")}</div>
|
||||
<p>${t("events.manage_step_setup_hint")}</p>
|
||||
</article>
|
||||
<article class="manage-step-card manage-step-card-${manageStatuses?.format || "pending"}">
|
||||
<div class="manage-step-head"><strong>${t("events.manage_step_format")}</strong>${renderManageStatusBadge(manageStatuses?.format || "pending")}</div>
|
||||
<article class="manage-step-card manage-step-card-${manageStatuses?.format?.status || "pending"} manage-step-card-link" data-target="manage-format" role="button" tabindex="0">
|
||||
<div class="manage-step-head"><strong>${t("events.manage_step_format")}</strong>${renderManageStatusBadge(manageStatuses?.format?.status || "pending")}</div>
|
||||
<div class="manage-step-meta">${escapeHtml(manageStatuses?.format?.detail || "")}</div>
|
||||
<p>${t("events.manage_step_format_hint")}</p>
|
||||
</article>
|
||||
<article class="manage-step-card manage-step-card-${manageStatuses?.generation || "pending"}">
|
||||
<div class="manage-step-head"><strong>${t("events.manage_step_generate")}</strong>${renderManageStatusBadge(manageStatuses?.generation || "pending")}</div>
|
||||
<article class="manage-step-card manage-step-card-${manageStatuses?.generation?.status || "pending"} manage-step-card-link" data-target="manage-generation" role="button" tabindex="0">
|
||||
<div class="manage-step-head"><strong>${t("events.manage_step_generate")}</strong>${renderManageStatusBadge(manageStatuses?.generation?.status || "pending")}</div>
|
||||
<div class="manage-step-meta">${escapeHtml(manageStatuses?.generation?.detail || "")}</div>
|
||||
<p>${t("events.manage_step_generate_hint")}</p>
|
||||
</article>
|
||||
<article class="manage-step-card manage-step-card-${manageStatuses?.live || "pending"}">
|
||||
<div class="manage-step-head"><strong>${t("events.manage_step_live")}</strong>${renderManageStatusBadge(manageStatuses?.live || "pending")}</div>
|
||||
<article class="manage-step-card manage-step-card-${manageStatuses?.live?.status || "pending"} manage-step-card-link" data-target="manage-live-results" role="button" tabindex="0">
|
||||
<div class="manage-step-head"><strong>${t("events.manage_step_live")}</strong>${renderManageStatusBadge(manageStatuses?.live?.status || "pending")}</div>
|
||||
<div class="manage-step-meta">${escapeHtml(manageStatuses?.live?.detail || "")}</div>
|
||||
<p>${t("events.manage_step_live_hint")}</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="race-stage-grid mt-16">
|
||||
<section class="panel">
|
||||
<section class="panel" id="manage-setup-participants">
|
||||
<div class="panel-header panel-header-with-pill">
|
||||
<h3>${t("events.select_participants")}</h3>
|
||||
<span class="pill">${selectedParticipantCount}</span>
|
||||
@@ -3974,7 +3992,7 @@ function renderEventManager(eventId) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<section class="panel" id="manage-setup-teams">
|
||||
<div class="panel-header panel-header-with-pill">
|
||||
<h3>${t("events.teams")}</h3>
|
||||
<span class="pill">${raceTeams.length}</span>
|
||||
@@ -4059,7 +4077,7 @@ function renderEventManager(eventId) {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="panel mt-16">
|
||||
<section class="panel mt-16" id="manage-format">
|
||||
<div class="panel-header"><h3>${t("events.race_format")}</h3></div>
|
||||
<div class="panel-body">
|
||||
<p>${t("events.race_format_intro")}</p>
|
||||
@@ -4295,7 +4313,7 @@ function renderEventManager(eventId) {
|
||||
<div class="race-summary-warnings">
|
||||
<strong>${t("events.summary_warnings_title")}</strong>
|
||||
<ul>
|
||||
${raceSummaryWarnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join("")}
|
||||
${raceSummaryWarnings.map((warning) => `<li><button class="summary-warning-link" type="button" data-target="${escapeHtml(warning.target)}">${escapeHtml(warning.message)}</button></li>`).join("")}
|
||||
</ul>
|
||||
</div>
|
||||
` : ""}
|
||||
@@ -4314,7 +4332,7 @@ function renderEventManager(eventId) {
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="panel-body race-actions-panel">
|
||||
<div class="panel-body race-actions-panel" id="manage-generation">
|
||||
<div class="panel-header panel-header-inline"><h3>${t("events.race_actions_title")}</h3></div>
|
||||
<p class="hint">${t("events.race_actions_hint")}</p>
|
||||
<div class="actions mt-16">
|
||||
@@ -4328,14 +4346,14 @@ function renderEventManager(eventId) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel mt-16">
|
||||
<section class="panel mt-16" id="manage-live-grid">
|
||||
<div class="panel-header"><h3>${t("events.grid_editor")}</h3></div>
|
||||
<div class="panel-body">
|
||||
${renderGridEditor(selectedGridSession)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel mt-16">
|
||||
<section class="panel mt-16" id="manage-live-results">
|
||||
<div class="panel-header"><h3>${t("events.practice_standings")}</h3></div>
|
||||
<div class="panel-body">
|
||||
${renderRaceStandingsTable(buildPracticeStandings(event), t("events.no_practice_results"))}
|
||||
@@ -4483,6 +4501,27 @@ function renderEventManager(eventId) {
|
||||
}
|
||||
`;
|
||||
|
||||
const bindManageJump = (node) => {
|
||||
const triggerJump = () => {
|
||||
const targetId = node.getAttribute("data-target") || "";
|
||||
const target = targetId ? document.getElementById(targetId) : null;
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
};
|
||||
node.addEventListener("click", triggerJump);
|
||||
node.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
triggerJump();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
eventManageArea.querySelectorAll(".summary-warning-link, .manage-step-card-link").forEach((node) => {
|
||||
bindManageJump(node);
|
||||
});
|
||||
|
||||
document.getElementById("eventBrandingForm")?.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
const form = new FormData(e.currentTarget);
|
||||
@@ -5820,7 +5859,7 @@ function renderGuide() {
|
||||
|
||||
<div class="guide-section-grid mt-16">
|
||||
${renderGuidePanel("guide.race_title", ["guide.race_1", "guide.race_2", "guide.race_3", "guide.race_4", "guide.race_4a", "guide.race_5", "guide.race_6", "guide.race_7", "guide.race_8", "guide.race_9", "guide.race_10"])}
|
||||
${renderGuidePanel("guide.manage_steps_title", ["guide.manage_steps_1", "guide.manage_steps_2", "guide.manage_steps_3", "guide.manage_steps_4", "guide.manage_steps_5", "guide.manage_steps_6"])}
|
||||
${renderGuidePanel("guide.manage_steps_title", ["guide.manage_steps_1", "guide.manage_steps_2", "guide.manage_steps_3", "guide.manage_steps_4", "guide.manage_steps_5", "guide.manage_steps_6", "guide.manage_steps_7", "guide.manage_steps_8"])}
|
||||
</div>
|
||||
|
||||
<div class="guide-section-grid mt-16">
|
||||
@@ -7781,24 +7820,24 @@ function getRaceSummaryWarnings(event, sessions, raceDrivers, raceTeams, selecte
|
||||
const hasFinals = sessions.some((session) => session.type === "final");
|
||||
const hasAnySession = sessions.length > 0;
|
||||
if (participantCount <= 0) {
|
||||
warnings.push(t("events.summary_warning_no_participants"));
|
||||
warnings.push({ message: t("events.summary_warning_no_participants"), target: "manage-setup-participants" });
|
||||
}
|
||||
if (!hasAnySession) {
|
||||
warnings.push(t("events.summary_warning_no_sessions"));
|
||||
warnings.push({ message: t("events.summary_warning_no_sessions"), target: "manage-session-plan" });
|
||||
}
|
||||
if (!isEndurancePreset && !hasQualifying) {
|
||||
warnings.push(t("events.summary_warning_no_qualifying"));
|
||||
warnings.push({ message: t("events.summary_warning_no_qualifying"), target: "manage-generation" });
|
||||
}
|
||||
if (!isEndurancePreset && !hasFinals) {
|
||||
warnings.push(t("events.summary_warning_no_finals"));
|
||||
warnings.push({ message: t("events.summary_warning_no_finals"), target: "manage-generation" });
|
||||
}
|
||||
if (isEndurancePreset && raceTeams.length <= 0) {
|
||||
warnings.push(t("events.summary_warning_no_teams"));
|
||||
warnings.push({ message: t("events.summary_warning_no_teams"), target: "manage-setup-teams" });
|
||||
}
|
||||
const minLapMs = Math.max(0, Number(cfg.minLapMs || 0) || 0);
|
||||
const maxLapMs = Math.max(0, Number(cfg.maxLapMs || 0) || 0);
|
||||
if (maxLapMs > 0 && maxLapMs <= minLapMs) {
|
||||
warnings.push(t("events.summary_warning_invalid_lap_window"));
|
||||
warnings.push({ message: t("events.summary_warning_invalid_lap_window"), target: "manage-format" });
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
@@ -7807,14 +7846,19 @@ function getRaceManageStatuses(event, sessions, raceDrivers, raceTeams, selected
|
||||
const cfg = event.raceConfig || {};
|
||||
const participantCount = cfg.participantsConfigured ? (cfg.driverIds || []).length : raceDrivers.length;
|
||||
const isEndurancePreset = selectedPreset?.id === "endurance";
|
||||
const hasQualifying = sessions.some((session) => session.type === "qualification");
|
||||
const hasFinals = sessions.some((session) => session.type === "final");
|
||||
const hasTeamRace = sessions.some((session) => session.type === "team_race");
|
||||
const hasStartedSession = sessions.some((session) => ["running", "finished", "stopped"].includes(String(session.status || "").toLowerCase()) || session.startedAt);
|
||||
const practiceCount = sessions.filter((session) => ["practice", "free_practice", "open_practice"].includes(session.type)).length;
|
||||
const qualCount = sessions.filter((session) => session.type === "qualification").length;
|
||||
const finalCount = sessions.filter((session) => session.type === "final").length;
|
||||
const teamCount = sessions.filter((session) => session.type === "team_race").length;
|
||||
const hasQualifying = qualCount > 0;
|
||||
const hasFinals = finalCount > 0;
|
||||
const hasTeamRace = teamCount > 0;
|
||||
const startedCount = sessions.filter((session) => ["running", "finished", "stopped"].includes(String(session.status || "").toLowerCase()) || session.startedAt).length;
|
||||
const practiceRows = buildPracticeStandings(event);
|
||||
const qualifyingRows = buildQualifyingStandings(event);
|
||||
const finalRows = buildFinalStandings(event);
|
||||
const lapWindowValid = Math.max(0, Number(cfg.maxLapMs || 0) || 0) > Math.max(0, Number(cfg.minLapMs || 0) || 0);
|
||||
const resultSourceCount = [practiceRows, qualifyingRows, finalRows].filter((rows) => rows.length > 0).length;
|
||||
|
||||
const setupStatus = participantCount > 0 && (!isEndurancePreset || raceTeams.length > 0)
|
||||
? "complete"
|
||||
@@ -7827,17 +7871,27 @@ function getRaceManageStatuses(event, sessions, raceDrivers, raceTeams, selected
|
||||
const generationReady = isEndurancePreset ? hasTeamRace : hasQualifying || hasFinals;
|
||||
const generationStatus = generationReady ? "complete" : sessions.length > 0 ? "attention" : "pending";
|
||||
|
||||
const liveStatus = hasStartedSession || practiceRows.length > 0 || qualifyingRows.length > 0 || finalRows.length > 0
|
||||
? "complete"
|
||||
: generationReady
|
||||
? "pending"
|
||||
: "pending";
|
||||
const liveStatus = startedCount > 0 || resultSourceCount > 0 ? "complete" : generationReady ? "pending" : "pending";
|
||||
|
||||
return {
|
||||
setup: setupStatus,
|
||||
format: formatStatus,
|
||||
generation: generationStatus,
|
||||
live: liveStatus,
|
||||
setup: {
|
||||
status: setupStatus,
|
||||
detail: `${participantCount} ${t("events.detail_drivers")} · ${raceTeams.length} ${t("events.detail_teams")}`,
|
||||
},
|
||||
format: {
|
||||
status: formatStatus,
|
||||
detail: `${selectedPreset?.label || t("events.preset_custom")} · ${((cfg.minLapMs || 0) / 1000).toFixed(1)} / ${((cfg.maxLapMs || 0) / 1000).toFixed(1)}s`,
|
||||
},
|
||||
generation: {
|
||||
status: generationStatus,
|
||||
detail: isEndurancePreset
|
||||
? `P ${practiceCount} · T ${teamCount} · ${sessions.length} ${t("events.detail_sessions")}`
|
||||
: `Q ${qualCount} · F ${finalCount} · ${sessions.length} ${t("events.detail_sessions")}`,
|
||||
},
|
||||
live: {
|
||||
status: liveStatus,
|
||||
detail: `${startedCount} ${t("events.detail_active")} · ${resultSourceCount} ${t("events.detail_results")}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user