// "What do I do next?" — answered from what the account ACTUALLY contains. // // There was already an onboarding wizard, but it is a one-time modal gated on a localStorage // flag: skip it once and it never returns, and it never knew whether you succeeded at anything. // Someone who closed it and then wondered how to get content on a screen had nothing left to go // on, which is exactly the confusion that got reported. // // A second tour would repeat that mistake. Tours are dismissed and forgotten, and they describe // the product rather than the account. This reads real state instead, so it cannot claim you have // done something you have not, it is still there tomorrow, and it disappears by itself once the // first screen is actually live — no nagging anyone who already knows the product. // // The steps are the shortest true path to a screen showing something: get a screen connected, // get media in, arrange it, put it on the screen. import { t } from '../i18n.js'; // Pure: given what the account holds, which steps are done and which is next. Separated from the // DOM so the logic that decides "you are finished" is testable — a checklist that congratulates // you too early is worse than none. export function computeSteps({ devices = [], content = [], playlists = [] } = {}) { const hasDevice = devices.length > 0; const hasContent = content.length > 0; const hasPlaylist = playlists.length > 0; // "On screen" is the only step that cannot be faked by creating an object and walking away: // some screen has to actually be pointed at something. const isAssigned = devices.some((d) => d.playlist_id || d.default_content_id || d.layout_id); const steps = [ { key: 'device', done: hasDevice, title: t('gs.device.title'), desc: t('gs.device.desc'), cta: t('gs.device.cta'), href: '#/', action: 'add-device', }, { key: 'content', done: hasContent, title: t('gs.content.title'), desc: t('gs.content.desc'), cta: t('gs.content.cta'), href: '#/content', }, { key: 'playlist', done: hasPlaylist, title: t('gs.playlist.title'), desc: t('gs.playlist.desc'), cta: t('gs.playlist.cta'), href: '#/playlists', }, { key: 'assign', done: isAssigned, title: t('gs.assign.title'), desc: t('gs.assign.desc'), cta: t('gs.assign.cta'), href: '#/', }, ]; // The NEXT step is the first unfinished one — in order, because each genuinely depends on the // one before it. Highlighting anything else would send someone to a screen they cannot use yet. const nextIndex = steps.findIndex((s) => !s.done); return { steps, nextIndex, complete: nextIndex === -1, doneCount: steps.filter((s) => s.done).length, }; } const DISMISS_KEY = 'rd_gs_dismissed'; export const isDismissed = () => localStorage.getItem(DISMISS_KEY) === '1'; export const dismiss = () => localStorage.setItem(DISMISS_KEY, '1'); export const undismiss = () => localStorage.removeItem(DISMISS_KEY); // Show it while there is still something to do and the user has not put it away. Deliberately // NOT gated on "is this a new account" — someone who has had the product a month and still has no // content is exactly who needs it. export function shouldShow(state) { return !state.complete && !isDismissed(); } export function render(host, state, { onAction } = {}) { if (!host) return; if (!shouldShow(state)) { host.innerHTML = ''; host.style.display = 'none'; return; } host.style.display = ''; const { steps, nextIndex, doneCount } = state; host.innerHTML = `
${t('gs.title')}
${t('gs.progress').replace('{done}', doneCount).replace('{total}', steps.length)}
${steps.map((s, i) => { const isNext = i === nextIndex; return `
${s.done ? '✓' : i + 1}
${s.title}
${!s.done ? `
${s.desc}
` : ''}
${!s.done && isNext ? `` : ''}
`; }).join('')}
`; host.querySelector('#gsDismiss')?.addEventListener('click', () => { dismiss(); host.innerHTML = ''; host.style.display = 'none'; }); host.querySelectorAll('[data-gs-step]').forEach((btn) => { btn.addEventListener('click', () => { const step = steps.find((s) => s.key === btn.dataset.gsStep); if (!step) return; // An in-page action (open the pairing dialog) beats navigating somewhere and leaving the // user to find the button again. if (step.action && onAction && onAction(step.action)) return; window.location.hash = step.href; }); }); }