/** * Settings → Integrations → Blocks, shortcodes & template tags. * * One tile per page element: shortcode, block markup or PHP, with a copy button and the * element's options. Names, descriptions and fields come from the element registry * (`config.elements`); add-ons list extra shortcodes through `streamsera_admin_data` (`shortcodes`). */ import { __, sprintf } from '@wordpress/i18n'; import { useMemo, useState } from '@wordpress/element'; import { registerSettingsSection } from '../../player/extensions'; import { Section, I } from '../ui'; import { config } from '../api'; type Field = { key: string; label: string; type: string; default: string | number | boolean; options: Array< [ string, string ] >; }; type Element = { id: string; label: string; description: string; block: string; pro?: boolean; fields: Field[]; }; type Format = 'shortcode' | 'block' | 'php'; type Item = { id: string; label: string; description: string; cat: string; icon: string; shortcode: string; tag: string; block: string; fields: Field[]; }; const CATS: Record< string, { label: string; icon: string } > = { player: { label: __( 'Player', 'streamsera' ), icon: 'player' }, stations: { label: __( 'Stations', 'streamsera' ), icon: 'broadcast' }, schedule: { label: __( 'Schedule', 'streamsera' ), icon: 'calendar' }, hosts: { label: __( 'Hosts', 'streamsera' ), icon: 'listeners' }, more: { label: __( 'More', 'streamsera' ), icon: 'grid' }, }; /** Element id → [ category, icon ]. */ const META: Record< string, [ string, string ] > = { player: [ 'player', 'player' ], 'now-playing': [ 'player', 'music' ], 'listen-button': [ 'player', 'button' ], 'recently-played': [ 'player', 'history' ], history: [ 'player', 'search' ], charts: [ 'player', 'like' ], station: [ 'stations', 'broadcast' ], 'station-list': [ 'stations', 'grid' ], schedule: [ 'schedule', 'calendar' ], 'now-next': [ 'schedule', 'clock' ], show: [ 'schedule', 'megaphone' ], 'show-list': [ 'schedule', 'list' ], host: [ 'hosts', 'user' ], 'host-list': [ 'hosts', 'listeners' ], }; const ALIAS: Record< string, string > = { '': 'player', stations: 'station-list', shows: 'show-list', hosts: 'host-list', }; function elementId( tag: string ) { const id = tag.replace( /^streamsera_?/, '' ).replace( /_/g, '-' ); return ALIAS[ id ] ?? id; } /** * Attributes written in an example shortcode, e.g. [ [ 'style', 'card|horizontal' ] ]. * @param sc */ function attrs( sc: string ): Array< [ string, string ] > { const out: Array< [ string, string ] > = []; sc.replace( /(\w+)="([^"]*)"/g, ( _m, k, v ) => { out.push( [ k, v ] ); return ''; } ); return out; } function fieldFor( item: Item, attr: string ) { const keys = 'id' === attr ? [ 'player' ] : [ attr, `show_${ attr }`, `${ attr }s` ]; return item.fields.find( ( f ) => keys.includes( f.key ) ); } /** * The shortcode with only the chosen attributes. * @param item * @param on */ function build( item: Item, on: string[] ) { const parts = attrs( item.shortcode ) .filter( ( [ k ] ) => on.includes( k ) ) .map( ( [ k, v ] ) => `${ k }="${ v.split( '|' )[ 0 ] }"` ); return `[${ item.tag }${ parts.length ? ' ' + parts.join( ' ' ) : '' }]`; } function codeFor( item: Item, format: Format, sc: string ) { if ( 'block' === format ) { return item.block ? `` : ''; } if ( 'php' === format ) { return 'player' === item.id ? ` 'card' ] ); ?>` : ``; } return sc; } /** * Coloured shortcode: tag, attribute names and values. * @param root0 * @param root0.code * @param root0.format */ function Pretty( { code, format }: { code: string; format: Format } ) { if ( 'shortcode' !== format ) { return { code }; } const [ , head = '', rest = '' ] = code.match( /^\[(\S+)\s?(.*)\]$/ ) || []; const parts: any[] = []; let last = 0; rest.replace( /(\w+)="([^"]*)"/g, ( m, k, v, at ) => { parts.push( rest.slice( last, at ) ); parts.push( { k }= "{ v }" ); last = at + m.length; return m; } ); parts.push( rest.slice( last ) ); return ( <> [ { head } { rest ? ' ' : '' } { parts } ] ); } function CopyBtn( { code, compact = false, }: { code: string; compact?: boolean; } ) { const [ done, setDone ] = useState( false ); const copy = async () => { try { await navigator.clipboard.writeText( code ); } catch { const ta = document.createElement( 'textarea' ); ta.value = code; document.body.appendChild( ta ); ta.select(); document.execCommand( 'copy' ); ta.remove(); } setDone( true ); setTimeout( () => setDone( false ), 1600 ); }; return ( ); } function Tile( { item, format, open, onToggle, }: { item: Item; format: Format; open: boolean; onToggle: () => void; } ) { const all = attrs( item.shortcode ); const defaults = all.map( ( [ k ] ) => k ); const [ on, setOn ] = useState< string[] >( defaults ); const sc = build( item, on ); const code = codeFor( item, format, sc ); const changed = on.join() !== defaults.join(); const flip = ( k: string ) => setOn( on.includes( k ) ? on.filter( ( x ) => x !== k ) : defaults.filter( ( x ) => x === k || on.includes( x ) ) ); const rows = all.map( ( [ k, v ] ) => { const f = fieldFor( item, k ); const values = v.includes( '|' ) ? v.split( '|' ).join( ' · ' ) : f?.options?.length ? f.options.map( ( o ) => o[ 0 ] ).join( ' · ' ) : ''; return { k, label: f?.label || k, values, val: v.split( '|' )[ 0 ], }; } ); const canOpen = rows.length > 0 && 'block' !== format; return (
{ item.label } { ( CATS[ item.cat ] || CATS.more ).label } { item.description ? ( { item.description } ) : null } { code ? (
) : (

{ __( 'Available as a shortcode only.', 'streamsera' ) }

) } { canOpen ? ( ) : null }
{ open && canOpen ? ( <>
{ __( 'Attribute', 'streamsera' ) } { __( 'What it does', 'streamsera' ) } { __( 'Value', 'streamsera' ) }
{ rows.map( ( r ) => { const active = on.includes( r.k ); return (
{ r.label } { r.values ? ( { r.values } ) : null } "{ r.val }"
); } ) }
{ __( 'Tick an attribute to add it to the shortcode — Copy takes the shortcode exactly as shown.', 'streamsera' ) } { changed ? ( ) : null }
) : null }
); } const CORE_SHORTCODES: Array< [ string, string ] > = [ [ __( 'Player', 'streamsera' ), '[streamsera id="1" layout="card"]' ], [ __( 'Now Playing', 'streamsera' ), '[streamsera_now_playing station="" artwork="1" listeners="0" compact="0"]', ], [ __( 'Listen Button', 'streamsera' ), '[streamsera_listen_button label="Listen live" target="listen" style="fill|outline|pill" size="md"]', ], [ __( 'Station', 'streamsera' ), '[streamsera_station station="" style="card|horizontal"]', ], [ __( 'Station List', 'streamsera' ), '[streamsera_stations columns="3"]' ], ]; const VISIBLE = 6; function EmbeddingSection() { const format: Format = 'shortcode'; const [ cat, setCat ] = useState( 'all' ); const [ q, setQ ] = useState( '' ); const [ open, setOpen ] = useState< string >( '' ); const [ all, setAll ] = useState( false ); const items = useMemo( () => { const elements = ( ( config as any ).elements || [] ) as Element[]; const list = [ ...CORE_SHORTCODES, ...( ( ( config as any ).shortcodes || [] ) as Array< [ string, string ] > ), ]; const seen = new Set< string >(); return list .map( ( [ label, sc ] ): Item | null => { const tag = ( sc.match( /^\[(\S+)/ ) || [] )[ 1 ] || ''; if ( ! tag || seen.has( tag ) ) { return null; } seen.add( tag ); const id = elementId( tag ); const el = elements.find( ( e ) => e.id === id ); const [ c, icon ] = META[ id ] || [ 'more', 'code' ]; return { id, label: el?.label || label, description: el?.description || '', cat: c, icon, shortcode: sc, tag, block: el?.block || '', fields: el?.fields || [], }; } ) .filter( Boolean ) as Item[]; }, [] ); const counts = useMemo( () => { const n: Record< string, number > = { all: items.length }; items.forEach( ( i ) => ( n[ i.cat ] = ( n[ i.cat ] || 0 ) + 1 ) ); return n; }, [ items ] ); const needle = q.trim().toLowerCase(); const shown = items.filter( ( i ) => ( 'all' === cat || i.cat === cat ) && ( ! needle || `${ i.label } ${ i.description } ${ i.tag } ${ i.block }` .toLowerCase() .includes( needle ) ) ); const limited = all || needle || 'all' !== cat; const visible = limited ? shown : shown.slice( 0, VISIBLE ); return (
setQ( e.target.value ) } placeholder={ __( 'Filter components', 'streamsera' ) } aria-label={ __( 'Filter components', 'streamsera' ) } />
{ [ 'all', ...Object.keys( CATS ) ] .filter( ( c ) => 'all' === c || counts[ c ] ) .map( ( c ) => ( ) ) }
{ visible.map( ( item ) => ( setOpen( open === item.tag ? '' : item.tag ) } /> ) ) } { ! shown.length ? (

{ __( 'Nothing matches that filter.', 'streamsera' ) }

) : null }
{ ! limited && shown.length > VISIBLE ? (
) : null }
); } registerSettingsSection( { id: 'embedding', label: __( 'Shortcodes', 'streamsera' ), icon: 'code', order: 10, group: 'integrations', hint: __( 'Copy a shortcode for any page', 'streamsera' ), Component: EmbeddingSection, } );