/** * The Embed dialog: add one RazorBooking item to this site. Used by Add to * site (booking form, reviews, gallery, FAQ), Services, Categories and Team. * * Three choices: add to an existing page, create a new draft page (both open * the block editor), or copy a shortcode / use the block or Elementor widget. * A service, category or team member is the Booking form with "Shows" (or * "Team member") set to it. * * The Booking confirmation is not embedded from here: the Thank-you page drawer * (features/thankYou) reuses insertInto() and EmbedCode for it. */ import { useState, type ReactNode } from 'react'; import { Alert, Button, Modal, ModalTitle, TextField } from '../ds'; import { ArrowLeft, ChevronRight, Code, FileText, Plus } from '../design-system/icons'; import { api, messageOf } from '../lib/api'; import { PagePicker } from './PagePicker'; import { ShortcodeBuilder } from './ShortcodeBuilder'; export type EmbedKind = 'all' | 'service' | 'category' | 'reviews' | 'gallery' | 'faq' | 'member' | 'confirmation'; export interface EmbedItem { kind: EmbedKind; /** Service, category or team member id. */ id?: string; /** What the owner calls it: "Booking form", "Reviews", "Blow-dry"… */ name: string; } /** The block (and Elementor widget) that renders it — titles as the inserter shows them. */ const BLOCK: Record< EmbedKind, string > = { all: 'RazorBooking Booking form', service: 'RazorBooking Booking form', category: 'RazorBooking Booking form', reviews: 'RazorBooking Reviews', gallery: 'RazorBooking Gallery', faq: 'RazorBooking FAQ', member: 'RazorBooking Booking form', confirmation: 'RazorBooking Booking confirmation', }; /** The shortcode, and what this item already fixes in it — the builder's starting point. */ function shortcodeOf( item: EmbedItem ): { tag: string; initial: Record< string, string > } { switch ( item.kind ) { case 'service': return { tag: 'razorbooking', initial: { service: String( item.id ) } }; case 'category': return { tag: 'razorbooking', initial: { category: String( item.id ) } }; case 'member': return { tag: 'razorbooking', initial: { staff: String( item.id ) } }; case 'reviews': case 'gallery': case 'faq': return { tag: `razorbooking_${ item.kind }`, initial: {} }; case 'confirmation': return { tag: 'razorbooking_confirmation', initial: {} }; default: return { tag: 'razorbooking', initial: {} }; } } /** What `pages/insert` is asked to put on the page (Razorbooking_Pages::block_for). */ function scopeFor( item: EmbedItem ): Record< string, unknown > { switch ( item.kind ) { case 'service': return { mode: 'service', service_ids: [ item.id ] }; case 'category': return { mode: 'category', category_id: item.id }; case 'member': return { mode: 'member', member_id: item.id }; case 'all': return { mode: 'all' }; default: return { mode: item.kind }; } } const PAGE_TITLE: Partial< Record< EmbedKind, string > > = { all: 'Book an appointment', reviews: 'Reviews', gallery: 'Gallery', faq: 'FAQ', confirmation: 'Thank you', }; /** * Add the item to the end of a page (0 creates a draft titled `title`) and * return the page's block-editor URL. A page that already has the Booking * confirmation is left as it is (Razorbooking_Pages::insert_into()). */ export async function insertInto( item: EmbedItem, pageId: number, title = '' ): Promise< string > { const result = await api.post< { edit_url: string } >( 'pages/insert', { page_id: pageId, title: title.trim() || PAGE_TITLE[ item.kind ] || item.name, scope: scopeFor( item ), } ); return result.edit_url; } type Step = 'choose' | 'existing' | 'new' | 'code'; export interface EmbedModalProps { item: EmbedItem | null; onClose: () => void; } export function EmbedModal( { item, onClose }: EmbedModalProps ) { const [ step, setStep ] = useState< Step >( 'choose' ); const [ pageId, setPageId ] = useState( '' ); const [ title, setTitle ] = useState( '' ); const [ working, setWorking ] = useState( false ); const [ error, setError ] = useState< string | null >( null ); if ( ! item ) return null; const close = () => { setStep( 'choose' ); setPageId( '' ); setTitle( '' ); setError( null ); onClose(); }; const insert = async ( target: 'existing' | 'new' ) => { setWorking( true ); setError( null ); try { window.location.href = await insertInto( item, target === 'new' ? 0 : Number( pageId ), title ); } catch ( caught ) { setError( messageOf( caught, 'Couldn’t add it. Try again.' ) ); setWorking( false ); } }; const back = ( ); return (
Add “{ item.name }” to your site { step === 'choose' ? (

Where should it go?

) : ( back ) }
{ step === 'choose' ? (
} title="Add to an existing page" text="Pick a page. We add it at the end and open the page in the editor so you can move it." onClick={ () => setStep( 'existing' ) } /> } title="Create a new page" text="We create a draft page with it already added and open it in the editor, ready for you to publish." onClick={ () => setStep( 'new' ) } /> } title="Shortcode, block, or Elementor" text="For page builders, widget areas, and themes, or to add it yourself." onClick={ () => setStep( 'code' ) } />
) : null } { step === 'existing' ? ( <> { error ? { error } : null }
) : null } { step === 'new' ? ( <> setTitle( e.target.value ) } hint="Saved as a draft. Visitors see it once you publish it." /> { error ? { error } : null }
) : null } { step === 'code' ? (
) : null }
); } export interface EmbedCodeProps { item: EmbedItem; /** Called when the "all shortcodes" link navigates away. */ onNavigate: () => void; } /** The item's shortcode builder, and where to find its block and Elementor widget. */ export function EmbedCode( { item, onNavigate }: EmbedCodeProps ) { return ( <> See all shortcodes and their options ); } interface ChoiceProps { icon: ReactNode; title: string; text: string; onClick: () => void; } function Choice( { icon, title, text, onClick }: ChoiceProps ) { return ( ); }