Split the Guide page into tabs, matching the Settings/Event Manager pattern

The page had grown to ~20 stacked panels on one very long scroll - the same problem
already fixed twice this session for Settings and Event Manager. Grouped into 5 tabs
(Getting started, Create a race, Run a race, Training & results, Video guides) using
the same activeGuideTab-as-module-state approach so the selection survives an in-place
re-render (e.g. a language switch). The always-visible overview cards at the top are
unchanged.

Also investigated a "feels slow" concern raised alongside this: measured the page's
actual render cost in a real browser (median 0.6ms, on par with or faster than Settings/
Timing) and confirmed the periodic tickClock/updateSyncStatus intervals never re-render
an idle Guide page. No performance issue found - the perceived slowness was the page's
length, which this change addresses directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-09-12 19:59:15 +02:00
parent 44f9b62610
commit 922bd704eb
10 changed files with 235 additions and 84 deletions

View File

@@ -1,3 +1,6 @@
let activeGuideTab = "start";
const GUIDE_TAB_IDS = ["start", "create", "run", "training", "videos"];
function renderGuidePanel(titleKey, itemKeys, extras = "", { t }) {
return `
<section class="panel">
@@ -23,7 +26,7 @@ function renderGuideOverviewCard(titleKey, blurbKey, { t }) {
export function renderGuideView({ dom, t }) {
dom.view.innerHTML = `
<section class="panel">
<section class="panel" id="guideOverview">
<div class="panel-header"><h3>${t("guide.title")}</h3></div>
<div class="panel-body">
<p>${t("guide.intro")}</p>
@@ -41,94 +44,121 @@ export function renderGuideView({ dom, t }) {
</div>
</section>
<div class="guide-section-grid mt-16">
<section class="panel">
<div class="panel-header"><h3>${t("guide.videos_title")}</h3></div>
<div class="guide-tabs" role="tablist">
<button type="button" class="guide-tab" data-guide-tab="start">${t("guide.tab_start")}</button>
<button type="button" class="guide-tab" data-guide-tab="create">${t("guide.tab_create")}</button>
<button type="button" class="guide-tab" data-guide-tab="run">${t("guide.tab_run")}</button>
<button type="button" class="guide-tab" data-guide-tab="training">${t("guide.tab_training")}</button>
<button type="button" class="guide-tab" data-guide-tab="videos">${t("guide.tab_videos")}</button>
</div>
<div data-guide-panel="start">
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.windows_title", ["guide.windows_1", "guide.windows_2", "guide.windows_3", "guide.windows_4", "guide.windows_5"], "", { t })}
${renderGuidePanel("guide.linux_title", ["guide.linux_1", "guide.linux_2", "guide.linux_3"], "", { t })}
</div>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.decoder_roles_title", ["guide.decoder_roles_1", "guide.decoder_roles_2", "guide.decoder_roles_3", "guide.decoder_roles_4", "guide.decoder_roles_5", "guide.decoder_roles_6", "guide.decoder_roles_7", "guide.decoder_roles_8", "guide.decoder_roles_9", "guide.decoder_roles_10", "guide.decoder_roles_11", "guide.decoder_roles_12", "guide.decoder_roles_13", "guide.decoder_roles_14", "guide.decoder_roles_15"], "", { t })}
${renderGuidePanel("guide.host_title", ["guide.host_1", "guide.host_2", "guide.host_3", "guide.host_4", "guide.host_5", "guide.host_6", "guide.host_7", "guide.host_8"], "", { t })}
</div>
<section class="panel mt-16">
<div class="panel-header"><h3>${t("guide.sqlite_title")}</h3></div>
<div class="panel-body">
<ul class="guide-list">
<li><a href="/docs/videos/connect-decoder.mkv" target="_blank" rel="noopener">${t("guide.video_connect_decoder")}</a></li>
<li><a href="/docs/videos/add-drivers-cars.mkv" target="_blank" rel="noopener">${t("guide.video_add_drivers_cars")}</a></li>
<li><a href="/docs/videos/create-race.mkv" target="_blank" rel="noopener">${t("guide.video_create_race")}</a></li>
<li><a href="/docs/videos/run-a-session.mkv" target="_blank" rel="noopener">${t("guide.video_run_session")}</a></li>
<li><a href="/docs/videos/settings-tabs.mkv" target="_blank" rel="noopener">${t("guide.video_settings_tabs")}</a></li>
<li><a href="/docs/videos/event-manager-tabs.mkv" target="_blank" rel="noopener">${t("guide.video_event_manager_tabs")}</a></li>
<li><a href="/docs/videos/event-schedule.mkv" target="_blank" rel="noopener">${t("guide.video_event_schedule")}</a></li>
<li>${t("guide.sqlite_1")}</li>
<li>${t("guide.sqlite_2")}</li>
<li>${t("guide.sqlite_3")}</li>
<li>${t("guide.sqlite_4")}</li>
<li>${t("guide.sqlite_5")}</li>
<li>${t("guide.sqlite_6")}</li>
</ul>
<p><a href="https://www.ammconverter.eu/docs/intro/quick-start/" target="_blank" rel="noreferrer">${t("guide.ammc_ref")}</a></p>
</div>
</section>
</div>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.race_day_title", ["guide.race_day_1", "guide.race_day_2", "guide.race_day_3", "guide.race_day_4", "guide.race_day_5", "guide.race_day_6", "guide.race_day_7", "guide.race_day_8"], "", { t })}
</div>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.sponsor_title", ["guide.sponsor_1", "guide.sponsor_2", "guide.sponsor_3", "guide.sponsor_4", "guide.sponsor_5", "guide.sponsor_6"], "", { t })}
${renderGuidePanel("guide.race_wizard_title", ["guide.race_wizard_1", "guide.race_wizard_2", "guide.race_wizard_3", "guide.race_wizard_4", "guide.race_wizard_5", "guide.race_wizard_6", "guide.race_wizard_7"], "", { t })}
</div>
<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"], "", { t })}
${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", "guide.manage_steps_9"], "", { t })}
</div>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.schedule_title", ["guide.schedule_1", "guide.schedule_2", "guide.schedule_3", "guide.schedule_4"], "", { t })}
</div>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.race_format_title", ["guide.race_format_0", "guide.race_format_1", "guide.race_format_2", "guide.race_format_3", "guide.race_format_4", "guide.race_format_5", "guide.race_format_6", "guide.race_format_7", "guide.race_format_8", "guide.race_format_9", "guide.race_format_10", "guide.race_format_11", "guide.race_format_11a", "guide.race_format_12", "guide.race_format_13", "guide.race_format_14"], "", { t })}
${renderGuidePanel("guide.validation_title", ["guide.validation_1", "guide.validation_2", "guide.validation_3", "guide.validation_4", "guide.validation_5", "guide.validation_6", "guide.validation_7", "guide.validation_8"], "", { t })}
</div>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.free_practice_title", ["guide.free_practice_1", "guide.free_practice_2", "guide.free_practice_3"], "", { t })}
${renderGuidePanel("guide.open_practice_title", ["guide.open_practice_1", "guide.open_practice_2", "guide.open_practice_3", "guide.open_practice_4", "guide.open_practice_5"], "", { t })}
</div>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.team_title", ["guide.team_1", "guide.team_2", "guide.team_3", "guide.team_4", "guide.team_5", "guide.team_6"], "", { t })}
${renderGuidePanel("guide.qualifying_title", ["guide.qualifying_1", "guide.qualifying_2", "guide.qualifying_3", "guide.qualifying_4", "guide.qualifying_5", "guide.qualifying_6", "guide.qualifying_7"], "", { t })}
</div>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.dashboard_title", ["guide.dashboard_1", "guide.dashboard_2", "guide.dashboard_3", "guide.dashboard_4"], "", { t })}
${renderGuidePanel("guide.training_title", ["guide.training_1", "guide.training_2", "guide.training_3", "guide.training_4", "guide.training_5", "guide.training_6", "guide.training_7", "guide.training_8", "guide.training_9", "guide.training_10", "guide.training_11", "guide.training_12", "guide.training_13", "guide.training_14", "guide.training_15", "guide.training_16", "guide.training_17", "guide.training_18", "guide.training_19", "guide.training_20", "guide.training_21", "guide.training_22", "guide.training_23", "guide.training_24", "guide.training_25", "guide.training_26", "guide.training_27", "guide.training_28", "guide.training_29", "guide.training_30", "guide.training_31"], "", { t })}
</div>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.archive_title", ["guide.archive_1", "guide.archive_2", "guide.archive_3", "guide.archive_4", "guide.archive_5", "guide.archive_6"], "", { t })}
</div>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.audio_title", ["guide.audio_1", "guide.audio_2", "guide.audio_3", "guide.audio_4", "guide.audio_5", "guide.audio_6", "guide.audio_7", "guide.audio_8", "guide.audio_9"], "", { t })}
</div>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.decoder_roles_title", ["guide.decoder_roles_1", "guide.decoder_roles_2", "guide.decoder_roles_3", "guide.decoder_roles_4", "guide.decoder_roles_5", "guide.decoder_roles_6", "guide.decoder_roles_7", "guide.decoder_roles_8", "guide.decoder_roles_9", "guide.decoder_roles_10", "guide.decoder_roles_11", "guide.decoder_roles_12", "guide.decoder_roles_13", "guide.decoder_roles_14", "guide.decoder_roles_15"], "", { t })}
${renderGuidePanel("guide.host_title", ["guide.host_1", "guide.host_2", "guide.host_3", "guide.host_4", "guide.host_5", "guide.host_6", "guide.host_7", "guide.host_8"], "", { t })}
</div>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.windows_title", ["guide.windows_1", "guide.windows_2", "guide.windows_3", "guide.windows_4", "guide.windows_5"], "", { t })}
${renderGuidePanel("guide.linux_title", ["guide.linux_1", "guide.linux_2", "guide.linux_3"], "", { t })}
</div>
<section class="panel mt-16">
<div class="panel-header"><h3>${t("guide.sqlite_title")}</h3></div>
<div class="panel-body">
<ul class="guide-list">
<li>${t("guide.sqlite_1")}</li>
<li>${t("guide.sqlite_2")}</li>
<li>${t("guide.sqlite_3")}</li>
<li>${t("guide.sqlite_4")}</li>
<li>${t("guide.sqlite_5")}</li>
<li>${t("guide.sqlite_6")}</li>
</ul>
<p><a href="https://www.ammconverter.eu/docs/intro/quick-start/" target="_blank" rel="noreferrer">${t("guide.ammc_ref")}</a></p>
<div data-guide-panel="create">
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.sponsor_title", ["guide.sponsor_1", "guide.sponsor_2", "guide.sponsor_3", "guide.sponsor_4", "guide.sponsor_5", "guide.sponsor_6"], "", { t })}
${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"], "", { t })}
</div>
</section>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.race_wizard_title", ["guide.race_wizard_1", "guide.race_wizard_2", "guide.race_wizard_3", "guide.race_wizard_4", "guide.race_wizard_5", "guide.race_wizard_6", "guide.race_wizard_7"], "", { t })}
${renderGuidePanel("guide.schedule_title", ["guide.schedule_1", "guide.schedule_2", "guide.schedule_3", "guide.schedule_4"], "", { t })}
</div>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.race_format_title", ["guide.race_format_0", "guide.race_format_1", "guide.race_format_2", "guide.race_format_3", "guide.race_format_4", "guide.race_format_5", "guide.race_format_6", "guide.race_format_7", "guide.race_format_8", "guide.race_format_9", "guide.race_format_10", "guide.race_format_11", "guide.race_format_11a", "guide.race_format_12", "guide.race_format_13", "guide.race_format_14"], "", { t })}
</div>
</div>
<div data-guide-panel="run">
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.race_day_title", ["guide.race_day_1", "guide.race_day_2", "guide.race_day_3", "guide.race_day_4", "guide.race_day_5", "guide.race_day_6", "guide.race_day_7", "guide.race_day_8"], "", { t })}
${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", "guide.manage_steps_9"], "", { t })}
</div>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.validation_title", ["guide.validation_1", "guide.validation_2", "guide.validation_3", "guide.validation_4", "guide.validation_5", "guide.validation_6", "guide.validation_7", "guide.validation_8"], "", { t })}
${renderGuidePanel("guide.dashboard_title", ["guide.dashboard_1", "guide.dashboard_2", "guide.dashboard_3", "guide.dashboard_4"], "", { t })}
</div>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.free_practice_title", ["guide.free_practice_1", "guide.free_practice_2", "guide.free_practice_3"], "", { t })}
${renderGuidePanel("guide.open_practice_title", ["guide.open_practice_1", "guide.open_practice_2", "guide.open_practice_3", "guide.open_practice_4", "guide.open_practice_5"], "", { t })}
</div>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.team_title", ["guide.team_1", "guide.team_2", "guide.team_3", "guide.team_4", "guide.team_5", "guide.team_6"], "", { t })}
${renderGuidePanel("guide.qualifying_title", ["guide.qualifying_1", "guide.qualifying_2", "guide.qualifying_3", "guide.qualifying_4", "guide.qualifying_5", "guide.qualifying_6", "guide.qualifying_7"], "", { t })}
</div>
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.audio_title", ["guide.audio_1", "guide.audio_2", "guide.audio_3", "guide.audio_4", "guide.audio_5", "guide.audio_6", "guide.audio_7", "guide.audio_8", "guide.audio_9"], "", { t })}
</div>
</div>
<div data-guide-panel="training">
<div class="guide-section-grid mt-16">
${renderGuidePanel("guide.training_title", ["guide.training_1", "guide.training_2", "guide.training_3", "guide.training_4", "guide.training_5", "guide.training_6", "guide.training_7", "guide.training_8", "guide.training_9", "guide.training_10", "guide.training_11", "guide.training_12", "guide.training_13", "guide.training_14", "guide.training_15", "guide.training_16", "guide.training_17", "guide.training_18", "guide.training_19", "guide.training_20", "guide.training_21", "guide.training_22", "guide.training_23", "guide.training_24", "guide.training_25", "guide.training_26", "guide.training_27", "guide.training_28", "guide.training_29", "guide.training_30", "guide.training_31"], "", { t })}
${renderGuidePanel("guide.archive_title", ["guide.archive_1", "guide.archive_2", "guide.archive_3", "guide.archive_4", "guide.archive_5", "guide.archive_6"], "", { t })}
</div>
</div>
<div data-guide-panel="videos">
<div class="guide-section-grid mt-16">
<section class="panel">
<div class="panel-header"><h3>${t("guide.videos_title")}</h3></div>
<div class="panel-body">
<ul class="guide-list">
<li><a href="/docs/videos/connect-decoder.mkv" target="_blank" rel="noopener">${t("guide.video_connect_decoder")}</a></li>
<li><a href="/docs/videos/add-drivers-cars.mkv" target="_blank" rel="noopener">${t("guide.video_add_drivers_cars")}</a></li>
<li><a href="/docs/videos/create-race.mkv" target="_blank" rel="noopener">${t("guide.video_create_race")}</a></li>
<li><a href="/docs/videos/run-a-session.mkv" target="_blank" rel="noopener">${t("guide.video_run_session")}</a></li>
<li><a href="/docs/videos/settings-tabs.mkv" target="_blank" rel="noopener">${t("guide.video_settings_tabs")}</a></li>
<li><a href="/docs/videos/event-manager-tabs.mkv" target="_blank" rel="noopener">${t("guide.video_event_manager_tabs")}</a></li>
<li><a href="/docs/videos/event-schedule.mkv" target="_blank" rel="noopener">${t("guide.video_event_schedule")}</a></li>
</ul>
</div>
</section>
</div>
</div>
`;
const guideTabButtons = Array.from(dom.view.querySelectorAll(".guide-tab"));
const guideTabPanels = Array.from(dom.view.querySelectorAll("[data-guide-panel]"));
const applyGuideTab = (tabId) => {
activeGuideTab = tabId;
guideTabButtons.forEach((button) => button.classList.toggle("active", button.dataset.guideTab === tabId));
guideTabPanels.forEach((panel) => { panel.hidden = panel.dataset.guidePanel !== tabId; });
};
guideTabButtons.forEach((button) => {
button.addEventListener("click", () => applyGuideTab(button.dataset.guideTab));
});
applyGuideTab(GUIDE_TAB_IDS.includes(activeGuideTab) ? activeGuideTab : "start");
}
export function renderOverlayPageView(deps) {