fix(mainstage): rename Home Page setting, start-page fallback + empty state (#975)

* fix(mainstage): rename Home Page setting, add start-page fallback + empty state

- Rename "Home Page" personalisation section to "Mainstage" across 9 locales
  so the heading matches the sidebar entry; add "mainstage" search keyword.
- Index route "/" now redirects to the first visible library item when the
  Mainstage sidebar entry is hidden, instead of stranding the app on a blank
  page. New pure resolveStartRoute() mirrors sidebar order + nav-mode gating.
- Show a guided empty state on Mainstage when every section is toggled off,
  with a CTA into Settings -> Personalisation.
- Unit tests for resolveStartRoute.

* docs(changelog): Mainstage rename, start-page fallback + empty state (#975)
This commit is contained in:
Frank Stellmacher
2026-06-04 02:12:52 +02:00
committed by GitHub
parent 3be8c367dd
commit 908c349cfd
24 changed files with 175 additions and 20 deletions
@@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest';
import { resolveStartRoute } from './sidebarNavReorder';
import { DEFAULT_SIDEBAR_ITEMS, type SidebarItemConfig } from '../../store/sidebarStore';
const hide = (items: SidebarItemConfig[], ...ids: string[]): SidebarItemConfig[] =>
items.map(i => (ids.includes(i.id) ? { ...i, visible: false } : i));
const only = (...ids: string[]): SidebarItemConfig[] =>
DEFAULT_SIDEBAR_ITEMS.map(i => ({ ...i, visible: ids.includes(i.id) }));
describe('resolveStartRoute', () => {
it('falls back to the first visible library item when Mainstage is hidden', () => {
const items = hide(DEFAULT_SIDEBAR_ITEMS, 'mainstage');
expect(resolveStartRoute(items, 'hub', false)).toBe('/new-releases');
});
it('skips Mainstage ("/") even if it is still flagged visible', () => {
// Resolver is only consulted when Mainstage is hidden, but it must never
// hand back "/" — that would redirect the index route onto itself.
expect(resolveStartRoute(DEFAULT_SIDEBAR_ITEMS, 'hub', false)).toBe('/new-releases');
});
it('returns null when no library item is visible (caller renders empty Mainstage)', () => {
const items = DEFAULT_SIDEBAR_ITEMS.map(i => ({ ...i, visible: false }));
expect(resolveStartRoute(items, 'hub', false)).toBeNull();
});
it('honours sidebar order — first visible entry wins', () => {
expect(resolveStartRoute(only('favorites', 'artists'), 'hub', false)).toBe('/artists');
expect(resolveStartRoute(only('favorites'), 'hub', false)).toBe('/favorites');
});
it('skips luckyMix when it is not available', () => {
const items = only('luckyMix', 'genres');
// separate mode surfaces luckyMix, but availability gate is off → next item
expect(resolveStartRoute(items, 'separate', false)).toBe('/genres');
expect(resolveStartRoute(items, 'separate', true)).toBe('/lucky-mix');
});
it('respects randomNavMode hub/separate gating', () => {
// randomPicker (Build a Mix) only exists in hub mode; randomMix only in separate.
expect(resolveStartRoute(only('randomPicker'), 'hub', false)).toBe('/random');
expect(resolveStartRoute(only('randomPicker'), 'separate', false)).toBeNull();
expect(resolveStartRoute(only('randomMix'), 'separate', false)).toBe('/random/mix');
expect(resolveStartRoute(only('randomMix'), 'hub', false)).toBeNull();
});
});
@@ -26,6 +26,31 @@ export function getSystemItemsForReorder(items: SidebarItemConfig[]): SidebarIte
return items.filter(cfg => ALL_NAV_ITEMS[cfg.id]?.section === 'system');
}
/**
* Resolve the route the app should open on "/" when the Mainstage entry is
* hidden from the sidebar. Mirrors the sidebar's own visible-library ordering
* (same filter + randomNavMode + luckyMix gating) and returns the first visible
* library item's route, skipping Mainstage itself ('/'). Returns null when no
* other library item is visible, so the caller can fall back to rendering the
* (empty) Mainstage rather than redirecting nowhere.
*/
export function resolveStartRoute(
items: SidebarItemConfig[],
randomNavMode: 'hub' | 'separate',
luckyMixAvailable: boolean,
): string | null {
const libraryConfigs = getLibraryItemsForReorder(items, randomNavMode).filter(cfg => {
if (!cfg.visible) return false;
if (cfg.id === 'luckyMix' && !luckyMixAvailable) return false;
return true;
});
for (const cfg of libraryConfigs) {
const to = ALL_NAV_ITEMS[cfg.id]?.to;
if (to && to !== '/') return to;
}
return null;
}
/** Same entries as in Settings toggles — safe to hide via drag-out. */
export function isSidebarNavItemUserHideable(id: string): boolean {
return Boolean(ALL_NAV_ITEMS[id]) && !CONSERVED_SIDEBAR_NAV_IDS.has(id);