/**
* Setup wizard — admin.php?page=streamsera#/setup.
*
* Three steps and a finish screen: Station (stream link, details) → Look (layout, style,
* colour) → Publish (add to a page, listen page, or by hand). A live preview of the real
* player sits beside every step. Finishing creates a station and a player, places the
* player where the user chose and marks `setup_completed`.
*/
import { __, sprintf } from '@wordpress/i18n';
import { useEffect, useMemo, useRef, useState } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import type { PlayerConfig } from '@shared/types';
import Player from '../../player/Player';
import { registerPage } from '../registry';
import { navigate } from '../router';
import {
config,
ss,
stations as stationsApi,
players as playersApi,
stationToPublic,
openPreview,
} from '../api';
import { I, CopyCode } from '../ui';
import { LayoutThumb } from './appearance';
import './setup.scss';
const cfgAny = config as any;
const STEPS = [
{ id: 'station', label: __( 'Station', 'streamsera' ) },
{ id: 'look', label: __( 'Look', 'streamsera' ) },
{ id: 'publish', label: __( 'Publish', 'streamsera' ) },
];
/** One-click colours for the play button and highlights. */
const COLORS: Array< [ string, string ] > = [
[ '#5b4cf5', __( 'Violet', 'streamsera' ) ],
[ '#2563eb', __( 'Blue', 'streamsera' ) ],
[ '#0e9f6e', __( 'Green', 'streamsera' ) ],
[ '#e8590c', __( 'Orange', 'streamsera' ) ],
[ '#d6336c', __( 'Pink', 'streamsera' ) ],
[ '#17171c', __( 'Black', 'streamsera' ) ],
];
/** How many styles show before "More styles". */
const SKINS_FIRST = 6;
type LogoMeta = { id: number; url: string; name?: string };
type Placement = 'page' | 'listen' | 'manual';
type Placed = {
kind: Placement;
view?: string;
edit?: string;
title?: string;
draft?: boolean;
error?: string;
};
const CONFETTI_COLORS = [
'#5b4cf5',
'#b6a6ff',
'#ffb020',
'#1faf5f',
'#e5484d',
'#2563eb',
];
/** One-shot confetti burst; unmounts itself after the animation. Hidden with reduced motion (CSS). */
function Confetti( { color }: { color: string } ) {
const [ gone, setGone ] = useState( false );
const pieces = useMemo( () => {
const colors = [ ...CONFETTI_COLORS, color ];
return Array.from( { length: 80 }, ( _, i ) => {
const strip = i % 3 === 0;
const size = 7 + Math.random() * 5;
return {
id: i,
left: Math.random() * 100,
w: strip ? 6 : size,
h: strip ? 12 + Math.random() * 6 : size,
round: i % 4 === 1,
color: colors[ i % colors.length ],
delay: Math.random() * 0.9,
dur: 2.6 + Math.random() * 1.8,
rot: ( Math.random() - 0.5 ) * 1440,
drift: ( Math.random() - 0.5 ) * 160,
};
} );
}, [ color ] );
useEffect( () => {
const t = window.setTimeout( () => setGone( true ), 5400 );
return () => window.clearTimeout( t );
}, [] );
if ( gone ) {
return null;
}
return (
{ pieces.map( ( c ) => (
) ) }
);
}
function useMediaPicker( onSelect: ( logo: LogoMeta ) => void ) {
return () => {
const wp = window.wp;
if ( ! wp?.media ) {
return;
}
const frame = wp.media( {
title: __( 'Station logo', 'streamsera' ),
multiple: false,
library: { type: 'image' },
} );
frame.on( 'select', () => {
const a = frame.state().get( 'selection' ).first().toJSON();
onSelect( {
id: a.id,
url: a.sizes?.medium?.url || a.url,
name: a.filename || a.title || '',
} );
} );
frame.open();
};
}
/** Upload a dropped image through the REST media endpoint. */
async function uploadImage( file: File ): Promise< LogoMeta > {
const body = new FormData();
body.append( 'file', file, file.name );
const a: any = await apiFetch( {
path: '/wp/v2/media',
method: 'POST',
body,
} );
const sizes = a.media_details?.sizes || {};
return {
id: a.id,
url: sizes.medium?.source_url || a.source_url,
name: file.name,
};
}
function detectFormat( detect: any, url: string ): string {
const ct = detect?.content_type || '';
if ( /aac/i.test( ct ) || /\.aac\b/i.test( url ) ) {
return 'aac';
}
if ( /ogg|opus/i.test( ct ) ) {
return 'ogg';
}
if ( /mpegurl|m3u8/i.test( ct ) || /\.m3u8/i.test( url ) ) {
return 'hls';
}
if ( /dash\+xml/i.test( ct ) || /\.mpd/i.test( url ) ) {
return 'dash';
}
return 'mp3';
}
/** The facts a detect result proves, as chip labels. Empty when the probe learned nothing. */
function detectFacts( detect: any, url: string ): string[] {
if ( ! detect || detect.failed ) {
return [];
}
const facts: string[] = [];
if ( detect.content_type || detect.server ) {
facts.push( detectFormat( detect, url ).toUpperCase() );
}
if ( detect.bitrate ) {
// translators: %d: stream bitrate in kilobits per second
facts.push( sprintf( __( '%d kbps', 'streamsera' ), detect.bitrate ) );
}
return facts;
}
/** "Artist — Title" from a detect result's metadata, if it carried one. */
function detectNowPlaying( detect: any ): string {
const m = detect?.metadata || {};
const title = typeof m.title === 'string' ? m.title : '';
const artist = typeof m.artist === 'string' ? m.artist : '';
return [ artist, title ].filter( Boolean ).join( ' — ' );
}
/**
* A friendlier default station name from the stream's own name: Icecast names often carry a
* tagline ("Groove Salad: a nicely chilled …") or a host suffix ("… [SomaFM]").
*/
export function cleanStreamName( raw: string ): string {
let n = String( raw || '' )
.replace( /\s*[\[(][^\])]*[\])]\s*$/, '' )
.trim();
const cut = n.search( /\s*(?::|\s[-–—|]\s)/ );
if ( cut > 2 ) {
n = n.slice( 0, cut );
}
return n.slice( 0, 80 ).trim();
}
/** A section header inside a step card: small icon tile + label. */
function FieldLabel( {
icon,
htmlFor,
children,
optional,
}: {
icon: string;
htmlFor?: string;
children: any;
optional?: boolean;
} ) {
const Tag: any = htmlFor ? 'label' : 'span';
return (
{ children }
{ optional ? (
{ __( '(optional)', 'streamsera' ) }
) : null }
);
}
function StepHead( {
n,
title,
text,
}: {
n: number;
title: string;
text: string;
} ) {
return (
{ sprintf(
// translators: 1: current step number, 2: number of steps
__( 'Step %1$d of %2$d', 'streamsera' ),
n,
STEPS.length
) }
{ title }
{ text }
);
}
export default function SetupWizard() {
const [ step, setStep ] = useState( 0 );
// Station
const [ url, setUrl ] = useState( '' );
const [ autoUrl, setAutoUrl ] = useState( '' );
const [ name, setName ] = useState( '' );
const [ genre, setGenre ] = useState( '' );
const [ logo, setLogo ] = useState< LogoMeta >( { id: 0, url: '' } );
const [ uploading, setUploading ] = useState( false );
const [ dragOver, setDragOver ] = useState( false );
const [ detect, setDetect ] = useState< any >( null );
const [ detecting, setDetecting ] = useState( false );
const [ existing, setExisting ] = useState< any[] | null >( null );
// Look
const [ layout, setLayout ] = useState< string >( config.defaults.layout );
const [ skin, setSkin ] = useState< string >( config.defaults.skin );
const [ color, setColor ] = useState( '' );
const [ allSkins, setAllSkins ] = useState( false );
// Publish
const canPages = !! cfgAny.canEditPages;
const canListen = !! config.canManage;
const [ placement, setPlacement ] = useState< Placement >(
canPages ? 'page' : 'manual'
);
const [ pages, setPages ] = useState< any[] | null >( null );
const [ pageId, setPageId ] = useState< string >( 'new' );
const [ newTitle, setNewTitle ] = useState< string >(
__( 'Listen live', 'streamsera' )
);
const [ position, setPosition ] = useState< 'prepend' | 'append' >(
'prepend'
);
const [ slug, setSlug ] = useState( 'listen' );
// Preview + finish
const [ device, setDevice ] = useState< 'desktop' | 'phone' >( 'desktop' );
const [ busy, setBusy ] = useState( false );
const [ error, setError ] = useState( '' );
const [ result, setResult ] = useState< {
station: number;
player: number;
} | null >( null );
const [ placed, setPlaced ] = useState< Placed | null >( null );
const done = !! result;
const scrollRef = useRef< HTMLDivElement | null >( null );
// Every step (and the finish screen) opens at its top.
useEffect( () => {
scrollRef.current?.scrollTo( { top: 0 } );
window.scrollTo( 0, 0 );
}, [ step, done ] );
const pickLogo = useMediaPicker( setLogo );
useEffect( () => {
stationsApi
.list()
.then( setExisting )
.catch( () => setExisting( [] ) );
}, [] );
// Pages for "Add it to a page", loaded the first time the Publish step opens.
useEffect( () => {
if ( step !== 2 || pages !== null || ! canPages ) {
return;
}
ss.searchPages( '' )
.then( ( list ) => {
const items = list || [];
setPages( items );
// Never pre-select an arbitrary page: only an obvious home page, otherwise a new page.
const home = items.find(
( p ) =>
p?.status === 'publish' &&
/^(home|front page|homepage)$/i.test(
( p?.title?.raw || '' ).trim()
)
);
setPageId( home ? String( home.id ) : 'new' );
} )
.catch( () => setPages( [] ) );
}, [ step, pages, canPages ] );
// Each check gets a number; a reply for an older link (typing, a second click) is dropped.
const detectRun = useRef( 0 );
const runDetect = async ( target?: string ) => {
const u = ( typeof target === 'string' ? target : url ).trim();
if ( ! u ) {
return;
}
const run = ++detectRun.current;
setDetecting( true );
setDetect( null );
setError( '' );
let r: any;
try {
r = ( await ss.detect( u ) ) || { failed: true };
} catch ( e: any ) {
r = { failed: true, message: e?.message || '' };
}
if ( run !== detectRun.current ) {
return;
}
setDetect( r );
if ( r?.name ) {
setName( ( n ) => n || cleanStreamName( r.name ) );
}
setDetecting( false );
};
// Check automatically once a complete-looking link has been pasted or typed.
useEffect( () => {
if ( ! autoUrl || ! /^https?:\/\/\S+\.\S+/i.test( autoUrl ) ) {
return;
}
const t = window.setTimeout( () => runDetect( autoUrl ), 500 );
return () => window.clearTimeout( t );
}, [ autoUrl ] ); // eslint-disable-line react-hooks/exhaustive-deps
const dropLogo = async ( e: any ) => {
e.preventDefault();
setDragOver( false );
const file: File | undefined = e.dataTransfer?.files?.[ 0 ];
if ( ! file || ! /^image\//.test( file.type ) ) {
return;
}
setUploading( true );
try {
setLogo( await uploadImage( file ) );
} catch ( err: any ) {
setError(
err?.message ||
__( 'Could not upload that image.', 'streamsera' )
);
}
setUploading( false );
};
const stationName = name.trim() || __( 'My Station', 'streamsera' );
const facts = detectFacts( detect, url );
const nowPlaying = detectNowPlaying( detect );
const checked = !! detect && ! detecting;
const found =
checked &&
( facts.length > 0 || !! detect.name || detect.reachable === true );
const previewStation = useMemo(
() =>
stationToPublic(
{
id: 0,
title: { raw: stationName },
meta: {
_streamsera_streams: [
{
url,
label: '',
format: detectFormat( detect, url ),
bitrate: detect?.bitrate || 0,
},
],
_streamsera_genre: genre,
_streamsera_slogan: '',
_streamsera_social: {},
},
},
logo.url
),
[ stationName, url, detect, genre, logo.url ]
);
// The player config the wizard saves: defaults + layout + style, with the chosen colour on top.
const cfg: PlayerConfig = useMemo( () => {
const sk: any = config.skins?.[ skin ] || {};
return {
...config.defaults,
layout,
skin,
tokens: {
...config.defaults.tokens,
...( sk.tokens || {} ),
...( color ? { primary: color } : {} ),
},
};
}, [ layout, skin, color ] );
const effectiveColor =
color ||
( cfg.tokens as any )?.primary ||
( config.defaults.tokens as any )?.primary ||
'#5b4cf5';
const skinEntries = Object.entries( config.skins || {} ) as Array<
[ string, any ]
>;
const shownSkins = useMemo( () => {
if ( allSkins ) {
return skinEntries;
}
const first = skinEntries.slice( 0, SKINS_FIRST );
// Keep the chosen style visible when the list is collapsed.
if ( ! first.some( ( [ k ] ) => k === skin ) ) {
const cur = skinEntries.find( ( [ k ] ) => k === skin );
if ( cur ) {
first.splice( SKINS_FIRST - 1, 1, cur );
}
}
return first;
}, [ allSkins, skin ] ); // eslint-disable-line react-hooks/exhaustive-deps
const home = String( cfgAny.siteUrl || '' ).replace( /\/$/, '' );
const host = home.replace( /^https?:\/\//, '' );
const pretty = cfgAny.permalinks !== false;
const cleanSlug =
slug
.toLowerCase()
.replace( /[^a-z0-9-]+/g, '-' )
.replace( /^-+|-+$/g, '' ) || 'listen';
const listenUrl = pretty
? `${ home }/${ cleanSlug }/`
: `${ home }/?streamsera_listen=1`;
const chosenPage = pages?.find( ( p ) => String( p.id ) === pageId );
const pagePath = ( p: any ) => {
try {
const u = new URL( p.link );
return u.host + u.pathname.replace( /\/$/, '' ) + u.search;
} catch ( e ) {
return host;
}
};
const previewAddress =
step < 2 && ! done
? host
: placement === 'listen'
? listenUrl.replace( /^https?:\/\//, '' ).replace( /\/$/, '' )
: placement === 'page'
? chosenPage
? pagePath( chosenPage )
: `${ host }/${ newTitle
.toLowerCase()
.replace( /[^a-z0-9]+/g, '-' )
.replace( /^-|-$/g, '' ) }`
: host;
const finish = async () => {
setBusy( true );
setError( '' );
const status = config.canPublish === false ? 'draft' : 'publish';
let created: { station: number; player: number };
try {
const station = await stationsApi.create( {
title: stationName,
status,
meta: {
_streamsera_streams: [
{
url: url.trim(),
label: detect?.bitrate
? `${ detect.bitrate } kbps`
: '',
format: detectFormat( detect, url ),
bitrate: detect?.bitrate || 0,
},
],
_streamsera_genre: genre.trim(),
_streamsera_logo_id: logo.id || 0,
_streamsera_metadata: {
provider: 'auto',
url: '',
mount: '',
fields: {},
},
},
} );
const player = await playersApi.create( {
title: stationName,
status,
meta: {
_streamsera_config: {
...cfg,
station: station.id,
stations: [],
},
},
} );
created = { station: station.id, player: player.id };
} catch ( e: any ) {
setError(
e?.message ||
__( 'Something went wrong while saving.', 'streamsera' )
);
setBusy( false );
return;
}
// Place the player. A failure here keeps the station and player; the finish screen says what happened.
let where: Placed = { kind: placement };
try {
if ( placement === 'page' ) {
const res = await ss.embedToPage( {
player: created.player,
...( chosenPage
? { page_id: chosenPage.id, position }
: {
title:
newTitle.trim() ||
__( 'Listen live', 'streamsera' ),
status: 'publish' as const,
} ),
} );
where = {
kind: 'page',
view: res?.view_link,
edit: res?.edit_link,
title: res?.title,
draft: res?.status && res.status !== 'publish',
};
} else if ( placement === 'listen' ) {
await ss.saveSettings( {
...( await ss.getSettings() ),
listen_enabled: true,
listen_slug: cleanSlug,
listen_player: created.player,
} );
where = { kind: 'listen', view: listenUrl };
}
} catch ( e: any ) {
where = {
kind: placement,
error:
e?.message ||
__( 'The player could not be placed.', 'streamsera' ),
};
}
try {
await ss.saveSettings( {
...( await ss.getSettings() ),
setup_completed: true,
} );
} catch ( e ) {
// Only managers can save settings; the checklist simply stays open for everyone else.
}
setPlaced( where );
setResult( created );
setBusy( false );
};
const next = () => {
if ( step < 2 ) {
setStep( step + 1 );
} else {
finish();
}
};
const canNext = step === 0 ? !! url.trim() && ! detecting : ! busy;
const nextHint =
step === 0 && ! url.trim()
? __( 'Paste a stream link to continue', 'streamsera' )
: step === 2
? __( 'Finishing creates one station and one player', 'streamsera' )
: '';
const previewNote = done
? __(
'Listeners can keep playing while they browse your site.',
'streamsera'
)
: step === 0
? __(
"This is how visitors will see your player. You'll pick the look in the next step.",
'streamsera'
)
: step === 1
? __(
'Want more control? Colours, components and behaviour are all in the builder after setup.',
'streamsera'
)
: __(
'Listeners can keep playing while they browse your site.',
'streamsera'
);
const renderPlayer = ( key: string ) => (
);
const doneLine = (): string => {
if ( ! placed || placed.error || placed.kind === 'manual' ) {
return __(
'Your station and player are saved. Copy the shortcode below to place it anywhere.',
'streamsera'
);
}
if ( placed.kind === 'listen' ) {
return sprintf(
// translators: %s: listen page address
__( 'Your listen page is live at %s.', 'streamsera' ),
listenUrl.replace( /^https?:\/\//, '' )
);
}
return placed.draft
? sprintf(
// translators: %s: page title
__(
'Your player is on the “%s” page, saved as a draft — publish it when you’re ready.',
'streamsera'
),
placed.title || ''
)
: sprintf(
// translators: %s: page title
__( 'Your player is now on the “%s” page.', 'streamsera' ),
placed.title || ''
);
};
return (
StreamSera
{ __( 'Setup', 'streamsera' ) }
{ STEPS.map( ( s, i ) => {
const isDone = done || i < step;
const isCurrent = ! done && i === step;
return (
step }
aria-current={
isCurrent ? 'step' : undefined
}
onClick={ () =>
i < step && setStep( i )
}
>
{ isDone ? (
) : (
i + 1
) }
{ s.label }
{ i < STEPS.length - 1 ? (
) : null }
);
} ) }
{ ! done ? (
navigate( 'dashboard' ) }
>
{ __( 'Skip setup', 'streamsera' ) }
) : null }
{ ! done ? (
{ step === 0 && (
<>
{ __(
'Stream link',
'streamsera'
) }
{ __(
'Works with Icecast, Shoutcast, HLS and most hosted streams. You’ll find it in your radio host’s dashboard, usually called “stream URL”.',
'streamsera'
) }
{ checked && (
{ found
? __(
'Stream found',
'streamsera'
)
: __(
"We couldn't read the stream details",
'streamsera'
) }
{ facts.map( ( f ) => (
{ f }
) ) }
{ found && nowPlaying ? (
<>
{ __(
'Now playing:',
'streamsera'
) }
{ ' ' }
{ nowPlaying }
>
) : found ? (
__(
'Your stream is reachable.',
'streamsera'
)
) : (
__(
'You can still continue — check that the link plays in your browser.',
'streamsera'
)
) }
) }
{ ( checked || !! name ) && (
<>
{
e.preventDefault();
setDragOver( true );
} }
onDragLeave={ () =>
setDragOver( false )
}
onDrop={ dropLogo }
>
{ logo.url ? (
) : (
) }
{ __(
'Station logo',
'streamsera'
) }
{ uploading
? __(
'Uploading…',
'streamsera'
)
: logo.url
? logo.name ||
__(
'Shown when a song has no artwork.',
'streamsera'
)
: __(
'Square image, 300 px or larger. Shown when a song has no artwork.',
'streamsera'
) }
{ logo.url
? __(
'Change',
'streamsera'
)
: __(
'Choose image',
'streamsera'
) }
{ logo.url ? (
setLogo( {
id: 0,
url: '',
} )
}
>
) : null }
>
) }
{ existing && existing.length > 0 && (
{ __(
'You already have stations. This adds a new one — to build a player for an existing station, use the Players page.',
'streamsera'
) }
) }
>
) }
{ step === 1 && (
<>
{ __( 'Layout', 'streamsera' ) }
{ Object.entries(
config.layouts || {}
).map( ( [ id, def ] ) => (
setLayout( id )
}
>
{ def.label }
{ def.description ? (
{
def.description
}
) : null }
) ) }
{ __( 'Style', 'streamsera' ) }
{ shownSkins.map(
( [ key, sk ] ) => {
const t =
sk.tokens || {};
return (
setSkin(
key
)
}
>
{ sk.label }
);
}
) }
{ skinEntries.length >
SKINS_FIRST && (
setAllSkins(
! allSkins
)
}
>
{ allSkins
? __(
'Fewer styles',
'streamsera'
)
: sprintf(
// translators: %d: number of hidden styles
__(
'%d more',
'streamsera'
),
skinEntries.length -
SKINS_FIRST
) }
) }
>
) }
{ step === 2 && (
<>
{ (
[
canPages && {
id: 'page',
icon: 'pages',
label: __(
'Add it to a page',
'streamsera'
),
desc: __(
'Place the player on a page you already have, or a new one.',
'streamsera'
),
},
canListen && {
id: 'listen',
icon: 'headphones',
label: __(
'Create a listen page',
'streamsera'
),
desc: __(
'A ready-made, full-screen page just for your station.',
'streamsera'
),
},
{
id: 'manual',
icon: 'code',
label: __(
'I’ll add it myself',
'streamsera'
),
desc: __(
'Use the block, a shortcode or your page builder.',
'streamsera'
),
},
].filter( Boolean ) as Array< {
id: Placement;
icon: string;
label: string;
desc: string;
} >
).map( ( o ) => (
setPlacement( o.id )
}
>
{ o.label }
{ o.desc }
) ) }
{ placement === 'page' && (
<>
{ __(
'Page',
'streamsera'
) }
setPageId(
e.target
.value
)
}
>
{ pages === null ? (
{ __(
'Loading pages…',
'streamsera'
) }
) : null }
{ (
pages || []
).map( ( p ) => (
{ ( p.title
?.raw ||
`#${ p.id }` ) +
( p.status &&
p.status !==
'publish'
? ` (${ p.status })`
: '' ) }
) ) }
{ __(
'+ Create a new page',
'streamsera'
) }
{ pageId === 'new' ? (
{ __(
'New page title',
'streamsera'
) }
setNewTitle(
e.target
.value
)
}
/>
) : (
{ __(
'Where on the page',
'streamsera'
) }
setPosition(
'prepend'
)
}
>
{ __(
'Top',
'streamsera'
) }
setPosition(
'append'
)
}
>
{ __(
'Bottom',
'streamsera'
) }
) }
{ pageId === 'new'
? __(
'We’ll create and publish the page with your player on it.',
'streamsera'
)
: __(
'We’ll add the StreamSera Player block. Nothing else on the page changes.',
'streamsera'
) }
>
) }
{ placement === 'listen' && (
<>
{ __(
'Page address',
'streamsera'
) }
{ pretty ? (
{ `${ host }/` }
setSlug(
e.target
.value
)
}
/>
) : (
{ listenUrl }
) }
{ __(
'A full-screen page with your player, what’s playing and share buttons — handy to link from social media.',
'streamsera'
) }
>
) }
{ placement === 'manual' && (
{ __(
'Block editor',
'streamsera'
) }
{ __(
'Search for “StreamSera Player” in the block inserter.',
'streamsera'
) }
{ __(
'Page builders',
'streamsera'
) }
{ __(
'Elementor, Divi, Bricks, Beaver Builder, Breakdance and Oxygen have a StreamSera element.',
'streamsera'
) }
{ __(
'Shortcode',
'streamsera'
) }
{ __(
'You’ll get it to copy on the next screen.',
'streamsera'
) }
) }
>
) }
{ error && (
{ error }
setError( '' ) }
>
) }
{ step > 0 ? (
setStep( step - 1 ) }
disabled={ busy }
>
{ __( 'Back', 'streamsera' ) }
) : null }
{ nextHint }
{ step === 1 ? (
{
setLayout( config.defaults.layout );
setSkin( config.defaults.skin );
setColor( '' );
setStep( 2 );
} }
>
{ __(
'Use the defaults',
'streamsera'
) }
) : null }
{ busy ? (
) : null }
{ step < 2
? __( 'Continue', 'streamsera' )
: busy
? __( 'Finishing…', 'streamsera' )
: __( 'Finish setup', 'streamsera' ) }
{ ! busy ? (
) : null }
) : (
{ __( 'You’re on air', 'streamsera' ) }
{ doneLine() }
{ placed?.error ? (
{ sprintf(
// translators: %s: error message
__(
'We couldn’t place the player automatically (%s). Use the shortcode below instead.',
'streamsera'
),
placed.error
) }
) : null }
{ renderPlayer( 'done' ) }
{ ( ! placed ||
placed.error ||
placed.kind === 'manual' ) &&
result ? (
{ __( 'Shortcode', 'streamsera' ) }
) : null }
{ __(
'Next, when you’re ready',
'streamsera'
) }
navigate( 'stations/new' ) }
>
{ __(
'Add more stations',
'streamsera'
) }
{ __(
'Run several stations from one player with a station switcher.',
'streamsera'
) }
{ config.canManage ? (
navigate( 'settings/station-pages' )
}
>
{ __(
'Turn on station pages',
'streamsera'
) }
{ __(
'Give each station its own page with now playing and share links.',
'streamsera'
) }
) : null }
navigate( 'players' ) }
>
{ __(
'Add it to more pages',
'streamsera'
) }
{ __(
'Use the block, a shortcode or your page builder anywhere.',
'streamsera'
) }
navigate( 'dashboard' ) }
>
{ __( 'Go to the dashboard', 'streamsera' ) }
) }
);
}
registerPage( {
id: 'setup',
label: __( 'Setup', 'streamsera' ),
Component: SetupWizard,
fullBleed: () => true,
} );