',
+ self::note( __( 'Editor preview — students see live availability on the published page.', 'unsupervised-schedular' ) ),
+ $dayHtml
+ );
+ }
+
+ public static function groupClasses(): string {
+ return sprintf(
+ '
%s
%s
%s
%s
25.00 CAD
',
+ 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(
+ '',
+ esc_html__( 'Email', 'unsupervised-schedular' )
+ );
+ $fields .= sprintf(
+ '',
+ esc_html__( 'Your name', 'unsupervised-schedular' )
+ );
+ $fields .= sprintf(
+ '',
+ esc_html__( 'Password', 'unsupervised-schedular' )
+ );
+ $fields .= sprintf(
+ '',
+ esc_attr__( 'Create Account', 'unsupervised-schedular' )
+ );
+
+ return sprintf(
+ '
%s
',
+ 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 '
' . esc_html( $text ) . '
';
+ }
+}
diff --git a/src/BlockRegistrar.php b/src/BlockRegistrar.php
new file mode 100644
index 0000000..a4da54e
--- /dev/null
+++ b/src/BlockRegistrar.php
@@ -0,0 +1,126 @@
+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>
+ */
+ 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 $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 $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 $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 $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' );
+ }
+}
diff --git a/src/Plugin.php b/src/Plugin.php
index bbe3cef..41124e4 100644
--- a/src/Plugin.php
+++ b/src/Plugin.php
@@ -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();
}
}
diff --git a/src/ShortcodeRegistrar.php b/src/ShortcodeRegistrar.php
index 74942fb..7a2bb15 100644
--- a/src/ShortcodeRegistrar.php
+++ b/src/ShortcodeRegistrar.php
@@ -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' ] );
diff --git a/tests/Unit/BlockPreviewTest.php b/tests/Unit/BlockPreviewTest.php
new file mode 100644
index 0000000..165d17f
--- /dev/null
+++ b/tests/Unit/BlockPreviewTest.php
@@ -0,0 +1,57 @@
+justReturn('');
+
+ $html = BlockPreview::login();
+
+ self::assertStringContainsString('class="us-login-form"', $html);
+ self::assertStringContainsString('name="log"', $html);
+ self::assertStringContainsString('name="pwd"', $html);
+ self::assertStringContainsString('us-editor-note', $html);
+ }
+
+ public function testRegistrationPreviewShowsADisabledSampleForm(): void
+ {
+ $html = BlockPreview::registration();
+
+ self::assertStringContainsString('class="us-register-form"', $html);
+ self::assertStringContainsString('id="us-reg-email"', $html);
+ self::assertStringContainsString('id="us-reg-name"', $html);
+ self::assertStringContainsString('id="us-reg-pass"', $html);
+ self::assertStringContainsString('disabled', $html);
+ self::assertStringContainsString('us-editor-note', $html);
+ }
+}
diff --git a/tests/Unit/BlockRegistrarTest.php b/tests/Unit/BlockRegistrarTest.php
new file mode 100644
index 0000000..9d4080a
--- /dev/null
+++ b/tests/Unit/BlockRegistrarTest.php
@@ -0,0 +1,167 @@
+preview;
+ }
+}
+
+class BlockRegistrarTest extends TestCase
+{
+ private BookingPage&Mockery\MockInterface $bookingPage;
+ private LoginPage&Mockery\MockInterface $loginPage;
+ private RegistrationPage&Mockery\MockInterface $registrationPage;
+ private GroupClassPage&Mockery\MockInterface $groupClassPage;
+ private TestableBlockRegistrar $registrar;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->bookingPage = Mockery::mock(BookingPage::class);
+ $this->loginPage = Mockery::mock(LoginPage::class);
+ $this->registrationPage = Mockery::mock(RegistrationPage::class);
+ $this->groupClassPage = Mockery::mock(GroupClassPage::class);
+
+ $this->registrar = new TestableBlockRegistrar(
+ $this->bookingPage,
+ $this->loginPage,
+ $this->registrationPage,
+ $this->groupClassPage,
+ );
+ }
+
+ public function testRegisterHooksBlockRegistrationOntoInit(): void
+ {
+ Actions\expectAdded('init')->once()->with([$this->registrar, 'registerBlocks']);
+
+ $this->registrar->register();
+ }
+
+ public function testRegisterBlocksRegistersAllFourBlocksWithAssets(): void
+ {
+ Functions\expect('wp_register_script')
+ ->once()
+ ->with(
+ BlockRegistrar::SCRIPT_HANDLE,
+ Mockery::pattern('~assets/js/blocks\.js$~'),
+ Mockery::type('array'),
+ USC_VERSION,
+ true
+ );
+ Functions\when('wp_style_is')->justReturn(false);
+ Functions\expect('wp_register_style')
+ ->once()
+ ->with(
+ BlockRegistrar::STYLE_HANDLE,
+ Mockery::pattern('~assets/css/frontend\.css$~'),
+ [],
+ USC_VERSION
+ );
+
+ $registered = [];
+ Functions\when('register_block_type')->alias(
+ static function (string $name, array $args) use (&$registered): bool {
+ $registered[$name] = $args;
+ return true;
+ }
+ );
+
+ $this->registrar->registerBlocks();
+
+ self::assertSame(
+ [
+ 'us-scheduler/booking',
+ 'us-scheduler/student-login',
+ 'us-scheduler/student-register',
+ 'us-scheduler/group-classes',
+ ],
+ array_keys($registered)
+ );
+
+ foreach ($registered as $args) {
+ self::assertSame(BlockRegistrar::SCRIPT_HANDLE, $args['editor_script']);
+ self::assertSame(BlockRegistrar::STYLE_HANDLE, $args['style']);
+ self::assertIsCallable($args['render_callback']);
+ }
+ }
+
+ public function testRegisterBlocksDoesNotReRegisterAnAlreadyRegisteredStyle(): void
+ {
+ Functions\when('wp_register_script')->justReturn(true);
+ Functions\when('wp_style_is')->justReturn(true);
+ Functions\expect('wp_register_style')->never();
+ Functions\when('register_block_type')->justReturn(true);
+
+ $this->registrar->registerBlocks();
+ }
+
+ public function testFrontEndRenderDelegatesToThePageObjects(): void
+ {
+ $this->registrar->preview = false;
+
+ $this->bookingPage->shouldReceive('render')->once()->with([])->andReturn('booking-html');
+ $this->loginPage->shouldReceive('render')->once()->with([])->andReturn('login-html');
+ $this->registrationPage->shouldReceive('render')->once()->with([])->andReturn('register-html');
+ $this->groupClassPage->shouldReceive('render')->once()->with([])->andReturn('group-html');
+
+ self::assertSame('booking-html', $this->registrar->renderBooking());
+ self::assertSame('login-html', $this->registrar->renderLogin());
+ self::assertSame('register-html', $this->registrar->renderRegistration());
+ self::assertSame('group-html', $this->registrar->renderGroupClasses());
+ }
+
+ public function testEditorPreviewRendersStaticMarkupWithoutTouchingThePages(): void
+ {
+ $this->registrar->preview = true;
+
+ Functions\when('wp_nonce_field')->justReturn('');
+
+ $this->bookingPage->shouldNotReceive('render');
+ $this->loginPage->shouldNotReceive('render');
+ $this->registrationPage->shouldNotReceive('render');
+ $this->groupClassPage->shouldNotReceive('render');
+
+ self::assertStringContainsString('us-booking-app', $this->registrar->renderBooking());
+ self::assertStringContainsString('us-login-form', $this->registrar->renderLogin());
+ self::assertStringContainsString('us-register-form', $this->registrar->renderRegistration());
+ self::assertStringContainsString('us-group-app', $this->registrar->renderGroupClasses());
+ }
+
+ public function testIsEditorPreviewIsFalseOutsideRestRequests(): void
+ {
+ // REST_REQUEST is undefined in the test process, so the real
+ // registrar must take the front-end path.
+ $registrar = new BlockRegistrar(
+ $this->bookingPage,
+ $this->loginPage,
+ $this->registrationPage,
+ $this->groupClassPage,
+ );
+
+ $this->bookingPage->shouldReceive('render')->once()->with([])->andReturn('live');
+
+ self::assertSame('live', $registrar->renderBooking());
+ }
+}