block attribute, per shortcode. */ private const SHORTCODES = [ self::FORM_SHORTCODE => [ 'block' => 'fxtr/reservation-form', 'attrs' => [ 'title' => 'title', 'submit_label' => 'submitLabel', 'show_details' => 'showDetails', ], ], self::MANAGE_SHORTCODE => [ 'block' => 'fxtr/manage-reservation', 'attrs' => [ 'title' => 'title' ], ], ]; public function register(): void { // Cloudflare Turnstile loader, enqueued by render.php only when the owner // fills in the Turnstile keys. Cloudflare serves it from its own host and // does not support a local copy. Listed in readme.txt, External services. wp_register_script( 'fxtr-turnstile', 'https://challenges.cloudflare.com/turnstile/v0/api.js', [], null, true ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion,WordPress.WP.EnqueuedResourceParameters.NotInFooter,PluginCheck.CodeAnalysis.EnqueuedResourceOffloading.OffloadedContent -- Cloudflare's own widget loader, versionless and only served by Cloudflare. wp_register_style( 'fxtr-booking', FXTR_URL . 'assets/css/booking.css', [], FXTR_VERSION ); $this->register_reservation_blocks(); } /** * The table reservation form (fxtr/reservation-form) and the page the * link in a reservation email opens (fxtr/manage-reservation). Both * scripts lean on reservation-common.js. */ private function register_reservation_blocks(): void { $this->register_datepicker(); wp_register_script( 'fxtr-reservation-common', FXTR_URL . 'assets/js/reservation-common.js', [ 'fxtr-datepicker' ], FXTR_VERSION, true ); wp_localize_script( 'fxtr-reservation-common', 'fxtrReservation', [ 'rest' => esc_url_raw( rest_url( 'fxtr/v1' ) ), 'twelveHour' => 1 === preg_match( '/[aAgh]/', (string) get_option( 'time_format', 'g:i a' ) ), 'i18n' => [ 'sending' => __( 'Sending…', 'flexatech-table-reservations' ), 'error' => __( 'Something went wrong.', 'flexatech-table-reservations' ), 'network' => __( 'Network error. Please try again.', 'flexatech-table-reservations' ), 'checkFields' => __( 'Please check the highlighted fields.', 'flexatech-table-reservations' ), 'loading' => __( 'Loading…', 'flexatech-table-reservations' ), 'loadError' => __( 'Could not load times', 'flexatech-table-reservations' ), 'noTimes' => __( 'No free tables that day', 'flexatech-table-reservations' ), 'selectTime' => __( 'Select a time', 'flexatech-table-reservations' ), 'pickDate' => __( 'Pick a date first', 'flexatech-table-reservations' ), 'nameRequired' => __( 'Please enter your name.', 'flexatech-table-reservations' ), 'phoneRequired' => __( 'Please enter your phone number.', 'flexatech-table-reservations' ), 'emailRequired' => __( 'Please enter your email address.', 'flexatech-table-reservations' ), 'fieldRequired' => __( 'Please fill in this field.', 'flexatech-table-reservations' ), 'boxRequired' => __( 'Please tick this box.', 'flexatech-table-reservations' ), 'fewerGuests' => __( 'Fewer guests', 'flexatech-table-reservations' ), 'moreGuests' => __( 'More guests', 'flexatech-table-reservations' ), /* translators: %d: the largest party that may book online. */ 'partyRange' => __( 'Enter 1 to %d guests. For a larger group, please call us.', 'flexatech-table-reservations' ), 'whenRequired' => __( 'Please choose a date and time.', 'flexatech-table-reservations' ), 'contactRequired' => __( 'Enter a phone number or an email so we can reach you.', 'flexatech-table-reservations' ), 'confirmed' => __( 'Your table is booked. We sent the details to your email.', 'flexatech-table-reservations' ), 'pending' => __( 'Thanks! We received your request and will confirm it shortly.', 'flexatech-table-reservations' ), /* translators: 1: number of guests, 2: date, 3: time. */ 'summary' => __( 'Table for %1$s on %2$s at %3$s', 'flexatech-table-reservations' ), 'statusPending' => __( 'Waiting for the restaurant to confirm. You can still change or cancel it below.', 'flexatech-table-reservations' ), 'statusOpen' => __( 'Confirmed. You can still change or cancel it below.', 'flexatech-table-reservations' ), 'isCancelled' => __( 'This reservation is cancelled.', 'flexatech-table-reservations' ), 'isClosed' => __( 'This reservation can no longer be changed online. Please call us.', 'flexatech-table-reservations' ), 'moved' => __( 'Your reservation is changed.', 'flexatech-table-reservations' ), 'cancelled' => __( 'Your reservation is cancelled.', 'flexatech-table-reservations' ), 'confirmCancel' => __( 'Cancel this reservation? This cannot be undone.', 'flexatech-table-reservations' ), 'noTimesDay' => __( 'No free tables on this day. Try another date below.', 'flexatech-table-reservations' ), 'noTimesShift' => __( 'No free times in this part of the day. Try All times.', 'flexatech-table-reservations' ), 'noOtherDays' => __( 'No free tables in the next few weeks.', 'flexatech-table-reservations' ), /* translators: %d: number of other free times that day. */ 'moreTimes' => __( '+%d more', 'flexatech-table-reservations' ), 'holding' => __( 'Holding your table…', 'flexatech-table-reservations' ), 'holdGone' => __( 'Your table was released because time ran out. Pick a time again.', 'flexatech-table-reservations' ), /* translators: %d: number of guests. */ 'partyOf' => __( 'Party of %d', 'flexatech-table-reservations' ), 'titleConfirmed' => __( 'Your table is booked', 'flexatech-table-reservations' ), 'titlePending' => __( 'Request received', 'flexatech-table-reservations' ), /* translators: %s: the guest's name from their Google account. */ 'signedAs' => __( 'Signed in as %s. Check your details below.', 'flexatech-table-reservations' ), ], ] ); wp_register_script( 'fxtr-reservation-form', FXTR_URL . 'assets/js/reservation-form.js', [ 'fxtr-reservation-common' ], FXTR_VERSION, true ); // Google's sign-in button, enqueued by the form only when the owner turned it on. $async = [ 'in_footer' => true, 'strategy' => 'async', ]; wp_register_script( 'fxtr-google-signin', 'https://accounts.google.com/gsi/client', [], null, $async ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- third-party loader, versionless by design. wp_register_script( 'fxtr-reservation-manage', FXTR_URL . 'assets/js/reservation-manage.js', [ 'fxtr-reservation-common' ], FXTR_VERSION, true ); wp_register_script( 'fxtr-reservation-blocks', FXTR_URL . 'assets/js/reservation-blocks.js', [ 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-server-side-render', 'wp-i18n' ], FXTR_VERSION, true ); wp_set_script_translations( 'fxtr-reservation-blocks', 'flexatech-table-reservations', FXTR_PATH . 'languages' ); register_block_type( FXTR_PATH . 'app/Blocks/reservation-form' ); register_block_type( FXTR_PATH . 'app/Blocks/manage-reservation' ); foreach ( array_keys( self::SHORTCODES ) as $tag ) { add_shortcode( $tag, static fn( mixed $atts ): string => self::shortcode( $tag, (array) $atts ) ); } } /** * Renders a shortcode as its block, so both print the same markup and * WordPress loads the block's script and style the same way. * * @param array $atts */ private static function shortcode( string $tag, array $atts ): string { $map = self::SHORTCODES[ $tag ]; $attrs = []; foreach ( $map['attrs'] as $name => $attr ) { if ( isset( $atts[ $name ] ) && is_scalar( $atts[ $name ] ) ) { $attrs[ $attr ] = sanitize_text_field( (string) $atts[ $name ] ); } } return render_block( [ 'blockName' => $map['block'], 'attrs' => $attrs, 'innerBlocks' => [], 'innerHTML' => '', 'innerContent' => [], ] ); } /** * A published page that shows a block, through the block or its * shortcode, or 0. */ public static function page_with( string $block ): int { $needles = [ '