Add Gutenberg dynamic-block wrappers for the front-end shortcodes
CI / No Debug Code (pull_request) Successful in 4s
CI / Tests (PHP 8.2) (pull_request) Successful in 52s
CI / Tests (PHP 8.1) (pull_request) Successful in 54s
CI / Tests (PHP 8.3) (pull_request) Successful in 1m29s
CI / Coding Standards (pull_request) Successful in 1m57s
CI / PHPStan (pull_request) Successful in 2m14s
CI / Build Plugin Zip (pull_request) Has been skipped
CI / No Debug Code (pull_request) Successful in 4s
CI / Tests (PHP 8.2) (pull_request) Successful in 52s
CI / Tests (PHP 8.1) (pull_request) Successful in 54s
CI / Tests (PHP 8.3) (pull_request) Successful in 1m29s
CI / Coding Standards (pull_request) Successful in 1m57s
CI / PHPStan (pull_request) Successful in 2m14s
CI / Build Plugin Zip (pull_request) Has been skipped
Wrap the four shortcodes (us_booking, us_student_login, us_student_register, us_group_classes) in dynamic blocks so pages can be previewed and styled in the block editor. Front-end rendering delegates to the same page objects the shortcodes use; in the editor's block-renderer REST preview a static, script-free BlockPreview is rendered instead (no live REST calls, redirects, or Stripe.js). The editor script (vanilla JS, no build step) registers each block with wp.serverSideRender previews and shortcode transforms; frontend.css is attached as the block style so previews pick up theme styling. Resolves #44 Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular;
|
||||
|
||||
/**
|
||||
* Static, script-free markup for the editor previews of the front-end blocks.
|
||||
*
|
||||
* The booking and group-class pages are populated by JavaScript on the live
|
||||
* site, and the registration page requires a valid invite token — none of
|
||||
* which exist inside the block editor. These previews reproduce the same
|
||||
* wrapper elements and CSS classes the live pages use, filled with
|
||||
* representative placeholder content, so themes can be styled against
|
||||
* realistic markup without firing REST calls, redirects, or Stripe.js.
|
||||
*/
|
||||
class BlockPreview {
|
||||
|
||||
public static function booking(): string {
|
||||
$days = [
|
||||
[
|
||||
'label' => __( 'Monday', 'unsupervised-schedular' ),
|
||||
'slots' => [
|
||||
[ '16:00–16:30', 30 ],
|
||||
[ '16:30–17:00', 30 ],
|
||||
],
|
||||
],
|
||||
[
|
||||
'label' => __( 'Wednesday', 'unsupervised-schedular' ),
|
||||
'slots' => [
|
||||
[ '17:00–17:45', 45 ],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$dayHtml = '';
|
||||
foreach ( $days as $day ) {
|
||||
$slotHtml = '';
|
||||
foreach ( $day['slots'] as $slot ) {
|
||||
$slotHtml .= sprintf(
|
||||
'<div class="us-slot"><span>%s (%d min)</span><button type="button" class="us-book-btn" disabled>%s</button></div>',
|
||||
esc_html( $slot[0] ),
|
||||
(int) $slot[1],
|
||||
esc_html__( 'Book', 'unsupervised-schedular' )
|
||||
);
|
||||
}
|
||||
|
||||
$dayHtml .= sprintf(
|
||||
'<div class="us-day"><h3 class="us-day-heading">%s</h3>%s</div>',
|
||||
esc_html( $day['label'] ),
|
||||
$slotHtml
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<div id="us-booking-app">%s<div id="us-slot-list">%s</div></div>',
|
||||
self::note( __( 'Editor preview — students see live availability on the published page.', 'unsupervised-schedular' ) ),
|
||||
$dayHtml
|
||||
);
|
||||
}
|
||||
|
||||
public static function groupClasses(): string {
|
||||
return sprintf(
|
||||
'<div id="us-group-app">%s<div id="us-group-list"><div class="us-class"><h3>%s</h3><p>%s</p><p>%s</p><p>25.00 CAD</p><button type="button" class="us-enrol-btn" disabled>%s</button></div></div></div>',
|
||||
self::note( __( 'Editor preview — students see live group classes on the published page.', 'unsupervised-schedular' ) ),
|
||||
esc_html__( 'Beginner Group Class', 'unsupervised-schedular' ),
|
||||
esc_html__( 'Saturdays 10:00–11:00', 'unsupervised-schedular' ),
|
||||
esc_html__( 'A sample class shown so the page can be styled.', 'unsupervised-schedular' ),
|
||||
esc_html__( 'Enrol', 'unsupervised-schedular' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The live login form renders fine without any request state, so the
|
||||
* preview includes the real template (the editing user is logged in, which
|
||||
* would otherwise short-circuit to an "already logged in" message).
|
||||
*/
|
||||
public static function login(): string {
|
||||
$error = '';
|
||||
|
||||
ob_start();
|
||||
include USC_PLUGIN_DIR . 'templates/frontend/login-page.php';
|
||||
|
||||
return self::note( __( 'Editor preview — logged-in visitors are offered a link to the booking page instead.', 'unsupervised-schedular' ) ) . (string) ob_get_clean();
|
||||
}
|
||||
|
||||
public static function registration(): string {
|
||||
$fields = sprintf(
|
||||
'<p><label for="us-reg-email">%s</label><input type="email" id="us-reg-email" value="[email protected]" readonly></p>',
|
||||
esc_html__( 'Email', 'unsupervised-schedular' )
|
||||
);
|
||||
$fields .= sprintf(
|
||||
'<p><label for="us-reg-name">%s</label><input type="text" id="us-reg-name"></p>',
|
||||
esc_html__( 'Your name', 'unsupervised-schedular' )
|
||||
);
|
||||
$fields .= sprintf(
|
||||
'<p><label for="us-reg-pass">%s</label><input type="password" id="us-reg-pass"></p>',
|
||||
esc_html__( 'Password', 'unsupervised-schedular' )
|
||||
);
|
||||
$fields .= sprintf(
|
||||
'<p><input type="submit" value="%s" disabled></p>',
|
||||
esc_attr__( 'Create Account', 'unsupervised-schedular' )
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<div class="us-register-form">%s<form>%s</form></div>',
|
||||
self::note( __( 'Editor preview — the live form requires a valid invite link and lists signup policies.', 'unsupervised-schedular' ) ),
|
||||
$fields
|
||||
);
|
||||
}
|
||||
|
||||
private static function note( string $text ): string {
|
||||
return '<p class="us-editor-note">' . esc_html( $text ) . '</p>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular;
|
||||
|
||||
use Unsupervised\Schedular\Auth\LoginPage;
|
||||
use Unsupervised\Schedular\Auth\RegistrationPage;
|
||||
use Unsupervised\Schedular\Booking\BookingPage;
|
||||
use Unsupervised\Schedular\GroupClass\GroupClassPage;
|
||||
|
||||
/**
|
||||
* Registers Gutenberg dynamic-block wrappers for the front-end shortcodes so
|
||||
* the pages can be previewed and styled inside the block editor.
|
||||
*
|
||||
* On the front end each block delegates to the same page object its shortcode
|
||||
* uses, so output is identical either way. Inside the editor (the
|
||||
* block-renderer REST preview used by wp.serverSideRender) a static preview
|
||||
* from BlockPreview is rendered instead — same markup and CSS classes, no
|
||||
* live REST calls, redirects, or Stripe.js.
|
||||
*/
|
||||
class BlockRegistrar {
|
||||
|
||||
public const SCRIPT_HANDLE = 'us-scheduler-blocks';
|
||||
public const STYLE_HANDLE = 'us-scheduler';
|
||||
|
||||
public function __construct(
|
||||
private BookingPage $bookingPage,
|
||||
private LoginPage $loginPage,
|
||||
private RegistrationPage $registrationPage,
|
||||
private GroupClassPage $groupClassPage,
|
||||
) {}
|
||||
|
||||
public function register(): void {
|
||||
add_action( 'init', [ $this, 'registerBlocks' ] );
|
||||
}
|
||||
|
||||
public function registerBlocks(): void {
|
||||
// The editor script registers the client side of each block (title,
|
||||
// icon, shortcode transform) and previews it via wp.serverSideRender.
|
||||
wp_register_script(
|
||||
self::SCRIPT_HANDLE,
|
||||
USC_PLUGIN_URL . 'assets/js/blocks.js',
|
||||
[ 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-server-side-render', 'wp-i18n' ],
|
||||
USC_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// The front-end stylesheet doubles as the block style so editor
|
||||
// previews look like the published page. ShortcodeRegistrar registers
|
||||
// the same handle on the front end, hence the guard.
|
||||
if ( ! wp_style_is( self::STYLE_HANDLE, 'registered' ) ) {
|
||||
wp_register_style( self::STYLE_HANDLE, USC_PLUGIN_URL . 'assets/css/frontend.css', [], USC_VERSION );
|
||||
}
|
||||
|
||||
foreach ( $this->blocks() as $name => $renderCallback ) {
|
||||
register_block_type(
|
||||
$name,
|
||||
[
|
||||
'api_version' => '3',
|
||||
'editor_script' => self::SCRIPT_HANDLE,
|
||||
'style' => self::STYLE_HANDLE,
|
||||
'render_callback' => $renderCallback,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block names mapped to their render callbacks.
|
||||
*
|
||||
* @return array<string, callable(array<string, mixed>): string>
|
||||
*/
|
||||
private function blocks(): array {
|
||||
return [
|
||||
'us-scheduler/booking' => [ $this, 'renderBooking' ],
|
||||
'us-scheduler/student-login' => [ $this, 'renderLogin' ],
|
||||
'us-scheduler/student-register' => [ $this, 'renderRegistration' ],
|
||||
'us-scheduler/group-classes' => [ $this, 'renderGroupClasses' ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the booking block.
|
||||
*
|
||||
* @param array<string, mixed> $attributes Block attributes (unused — the blocks have none yet).
|
||||
*/
|
||||
public function renderBooking( array $attributes = [] ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
|
||||
return $this->isEditorPreview() ? BlockPreview::booking() : $this->bookingPage->render( [] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the student-login block.
|
||||
*
|
||||
* @param array<string, mixed> $attributes Block attributes (unused — the blocks have none yet).
|
||||
*/
|
||||
public function renderLogin( array $attributes = [] ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
|
||||
return $this->isEditorPreview() ? BlockPreview::login() : $this->loginPage->render( [] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the student-registration block.
|
||||
*
|
||||
* @param array<string, mixed> $attributes Block attributes (unused — the blocks have none yet).
|
||||
*/
|
||||
public function renderRegistration( array $attributes = [] ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
|
||||
return $this->isEditorPreview() ? BlockPreview::registration() : $this->registrationPage->render( [] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the group-classes block.
|
||||
*
|
||||
* @param array<string, mixed> $attributes Block attributes (unused — the blocks have none yet).
|
||||
*/
|
||||
public function renderGroupClasses( array $attributes = [] ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
|
||||
return $this->isEditorPreview() ? BlockPreview::groupClasses() : $this->groupClassPage->render( [] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this render is the editor's block-renderer REST preview rather
|
||||
* than a real front-end page render. Front-end template rendering never
|
||||
* happens inside a REST request, so REST_REQUEST is a reliable signal.
|
||||
*/
|
||||
protected function isEditorPreview(): bool {
|
||||
return defined( 'REST_REQUEST' ) && (bool) constant( 'REST_REQUEST' );
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -4,10 +4,14 @@ declare(strict_types=1);
|
||||
namespace Unsupervised\Schedular;
|
||||
|
||||
use Unsupervised\Schedular\Auth\InviteRepository;
|
||||
use Unsupervised\Schedular\Auth\LoginPage;
|
||||
use Unsupervised\Schedular\Auth\RegistrationPage;
|
||||
use Unsupervised\Schedular\Auth\RoleManager;
|
||||
use Unsupervised\Schedular\Booking\BookingPage;
|
||||
use Unsupervised\Schedular\Availability\AvailabilityRepository;
|
||||
use Unsupervised\Schedular\Booking\BookingRepository;
|
||||
use Unsupervised\Schedular\GroupClass\EnrollmentRepository;
|
||||
use Unsupervised\Schedular\GroupClass\GroupClassPage;
|
||||
use Unsupervised\Schedular\Offering\OfferingRepository;
|
||||
use Unsupervised\Schedular\Payment\BillingMethodResolver;
|
||||
use Unsupervised\Schedular\Payment\PaymentRepository;
|
||||
@@ -48,9 +52,17 @@ class Plugin {
|
||||
$stripe = new StripeGateway( $settings );
|
||||
$paymentService = new PaymentService( $paymentRepo, $resolver, new ReceiptMailer(), $bookings, $enrollments, $settings, $stripe );
|
||||
|
||||
// The shortcode and block wrappers share the same page objects so
|
||||
// front-end output is identical whichever way a page embeds them.
|
||||
$bookingPage = new BookingPage();
|
||||
$loginPage = new LoginPage();
|
||||
$registrationPage = new RegistrationPage( $invites, $policies, $policyVersions, $acceptances );
|
||||
$groupClassPage = new GroupClassPage();
|
||||
|
||||
( new RoleManager() )->register();
|
||||
( new AdminMenu( $availability, $bookings, $offerings, $questions, $policies, $policyVersions, $policyService, $invites, $enrollments, $settings, $paymentRepo, $paymentService, $resolver ) )->register();
|
||||
( new RestRegistrar( $availability, $bookings, $offerings, $questions, $policies, $policyVersions, $policyService, $registrationGate, $enrollments, $paymentService ) )->register();
|
||||
( new ShortcodeRegistrar( $invites, $policies, $policyVersions, $acceptances ) )->register();
|
||||
( new ShortcodeRegistrar( $bookingPage, $loginPage, $registrationPage, $groupClassPage ) )->register();
|
||||
( new BlockRegistrar( $bookingPage, $loginPage, $registrationPage, $groupClassPage ) )->register();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,34 +3,20 @@ declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular;
|
||||
|
||||
use Unsupervised\Schedular\Auth\InviteRepository;
|
||||
use Unsupervised\Schedular\Auth\LoginPage;
|
||||
use Unsupervised\Schedular\Auth\RegistrationPage;
|
||||
use Unsupervised\Schedular\Booking\BookingPage;
|
||||
use Unsupervised\Schedular\GroupClass\GroupClassPage;
|
||||
use Unsupervised\Schedular\Payment\StudioSettings;
|
||||
use Unsupervised\Schedular\Policy\AcceptanceRepository;
|
||||
use Unsupervised\Schedular\Policy\PolicyRepository;
|
||||
use Unsupervised\Schedular\Policy\PolicyVersionRepository;
|
||||
|
||||
class ShortcodeRegistrar {
|
||||
|
||||
private BookingPage $bookingPage;
|
||||
private LoginPage $loginPage;
|
||||
private RegistrationPage $registrationPage;
|
||||
private GroupClassPage $groupClassPage;
|
||||
|
||||
public function __construct(
|
||||
InviteRepository $invites,
|
||||
PolicyRepository $policies,
|
||||
PolicyVersionRepository $policyVersions,
|
||||
AcceptanceRepository $acceptances,
|
||||
) {
|
||||
$this->bookingPage = new BookingPage();
|
||||
$this->loginPage = new LoginPage();
|
||||
$this->registrationPage = new RegistrationPage( $invites, $policies, $policyVersions, $acceptances );
|
||||
$this->groupClassPage = new GroupClassPage();
|
||||
}
|
||||
private BookingPage $bookingPage,
|
||||
private LoginPage $loginPage,
|
||||
private RegistrationPage $registrationPage,
|
||||
private GroupClassPage $groupClassPage,
|
||||
) {}
|
||||
|
||||
public function register(): void {
|
||||
add_shortcode( 'us_booking', [ $this->bookingPage, 'render' ] );
|
||||
|
||||
Reference in New Issue
Block a user