/**
* 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 (
Where should it go?