diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b61ea0..369b394 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ each change under the current top section as you work. ## [1.3.1] +### Added +- An **Account** block (`[us_account]`) showing who is signed in — their name, their email, the students they book for if that is anyone besides themselves — and a **Sign out** link. Signing out returns to the login page chosen in the block, or to the page the visitor was already on when none is set, so putting it in a site header does not also move people somewhere. To a signed-out visitor it shows a **Sign in** link when a login page is chosen, and nothing at all when one is not: a panel about who is signed in has nothing to tell a stranger, and a notice they cannot act on is just clutter in a header. + ### Changed - A student's **name and birth year are now required**, marked in the form the same way a required registration question is and enforced on the server whichever way they were submitted. On signup the requirement applies only once the parent/guardian box is ticked, so registering for yourself is unaffected. A student block you have started filling in is now reported back to you rather than silently dropped when the name is missing — only a completely untouched spare block is still ignored. - Signup and the profile page now ask for a **birth year** rather than a full date of birth — a four-digit year between 1900 and the current year, with anything else discarded rather than stored. Students added before this change keep showing a birth year, derived from the date already on file; that old full date is then dropped the first time the record is saved, so the studio ends up holding only what it now asks for. No bulk purge runs, so a site wanting the remaining old dates gone should clear the `us_date_of_birth` user meta directly. diff --git a/assets/css/frontend.css b/assets/css/frontend.css index 24b8cbe..02a5ee3 100644 --- a/assets/css/frontend.css +++ b/assets/css/frontend.css @@ -525,6 +525,30 @@ } } +/* + * The account panel: who is signed in, and the way out. Sized to sit in a + * header or sidebar, so the rules stay minimal and inherit the theme's type — + * a block that lands in a site header should look like it belongs there. + */ +.us-account p { + margin: 0 0 4px; +} + +.us-account-name { + font-weight: 600; +} + +.us-account-email, +.us-account-students { + display: block; + font-size: 0.9em; + opacity: 0.75; +} + +.us-account-actions { + margin-top: 8px; +} + /* Shown only in block-editor previews (see BlockPreview). */ .us-editor-note { font-size: 0.85em; diff --git a/assets/js/blocks.js b/assets/js/blocks.js index d643f28..dcaf81b 100644 --- a/assets/js/blocks.js +++ b/assets/js/blocks.js @@ -307,6 +307,28 @@ }) ), }, + { + name: 'us-scheduler/account', + title: __('Account', 'unsupervised-schedular'), + description: __('Shows who is signed in, who they book for, and a sign out link. Renders nothing for signed-out visitors unless a login page is chosen.', 'unsupervised-schedular'), + icon: 'admin-users', + keywords: ['account', 'sign out', 'log out', 'signed in', 'profile'], + shortcode: 'us_account', + attributes: { + loginPageId: { type: 'number', default: 0 }, + }, + inspector: (attributes, setAttributes) => el( + PanelBody, + { title: __('Signing in and out', 'unsupervised-schedular') }, + el(PageSelect, { + label: __('Login page', 'unsupervised-schedular'), + help: __('Where signing out returns to, and where signed-out visitors are offered a link to sign in. Without one, signing out returns to the current page and signed-out visitors see nothing.', 'unsupervised-schedular'), + defaultLabel: __('Stay on the current page', 'unsupervised-schedular'), + value: attributes.loginPageId, + onChange: (loginPageId) => setAttributes({ loginPageId }), + }) + ), + }, ]; blocks.forEach((def) => { diff --git a/docs/features/editor-blocks.md b/docs/features/editor-blocks.md index 3383256..c30a16d 100644 --- a/docs/features/editor-blocks.md +++ b/docs/features/editor-blocks.md @@ -1,8 +1,8 @@ # Editor Blocks -Gutenberg dynamic-block wrappers for the plugin's four front-end shortcodes, -so the pages can be previewed and styled inside the block editor instead of -appearing as grey shortcode text. +Gutenberg dynamic-block wrappers for the plugin's front-end shortcodes, so the +pages can be previewed and styled inside the block editor instead of appearing +as grey shortcode text. ## Blocks @@ -12,6 +12,8 @@ appearing as grey shortcode text. | `us-scheduler/student-login` | `[us_student_login]` | `Auth\LoginPage::render()` | | `us-scheduler/student-register` | `[us_student_register]` | `Auth\RegistrationPage::render()` | | `us-scheduler/group-classes` | `[us_group_classes]` | `GroupClass\GroupClassPage::render()` | +| `us-scheduler/family` | `[us_family]` | `Guardian\FamilyPage::render()` | +| `us-scheduler/account` | `[us_account]` | `Auth\AccountPage::render()` | The shortcodes remain registered for back-compat; blocks and shortcodes share the same page objects (constructed once in `Plugin::boot()`), so front-end @@ -21,7 +23,7 @@ transform. ## Block options -Four blocks have sidebar (inspector) options: +Most blocks have sidebar (inspector) options: | Block | Attribute | Default | Effect | |---|---|---|---| @@ -34,6 +36,8 @@ Four blocks have sidebar (inspector) options: | `us-scheduler/student-login` | `autoRedirect` (boolean) | `false` | Send logged-in visitors straight to the booking page instead of showing the link. Does nothing until a booking page is chosen. | | `us-scheduler/student-register` | `loginPageId` (number) | `0` | Page students continue to once registration finishes — the "Sign in to your account" link after they confirm their email, and the "Continue to your account" link an invited student gets on the spot. `0` = the WordPress login screen for the confirmation link, and no link at all for the (already signed-in) invited student. Shortcode equivalent: `[us_student_register login_page_id="…"]`. | | `us-scheduler/student-register` | `autoRedirect` (boolean) | `false` | Send students straight to that page instead of showing the link. Does nothing until a page is chosen — there is no login-screen fallback here. | +| `us-scheduler/family` | `loginPageId` (number) | `0` | Where visitors who are not signed in are sent to log in. Shortcode equivalent: `[us_family login_page_id="…"]`. | +| `us-scheduler/account` | `loginPageId` (number) | `0` | Where signing out returns to, and where a signed-out visitor is offered a **Sign in** link. `0` = signing out returns to the current page, and a signed-out visitor sees **nothing at all** — see below. Shortcode equivalent: `[us_account login_page_id="…"]`. | | `us-scheduler/group-classes` | `offeringId` (number) | `0` | Restrict the page to a single group class, for embedding on a page dedicated to that class. The class description is then omitted — only the schedule, instructor, price and enrolment controls are shown, so the surrounding page's own copy is not repeated. `0` = browse all classes, descriptions included. Shortcode equivalent: `[us_group_classes offering="…"]`. | The page selects list all published pages; if a chosen page is later deleted, @@ -105,6 +109,10 @@ placeholder content: - **Login** — the real `templates/frontend/login-page.php` template (it has no request-state dependencies). - **Registration** — a disabled sample of the `.us-register-form` fields. +- **Account** — a populated sample panel. Deliberately populated whatever the + editor user's own state: on the published page a signed-out visitor may see + nothing at all, and an empty box tells the person placing the block nothing + about where it will sit. Each preview starts with a `.us-editor-note` paragraph explaining what the published page shows instead. The note class only appears in editor previews. @@ -118,5 +126,23 @@ published page shows instead. The note class only appears in editor previews. and fallbacks. - `tests/Unit/Auth/LoginPageTest.php` — logged-in booking-link targets and fallbacks. +- `tests/Unit/Auth/AccountPageTest.php` — what each visitor sees, the + sign-out redirect target, and the signed-out empty render. - `tests/Unit/BlockPreviewTest.php` — preview markup mirrors the live CSS classes/ids and includes the editor note. + +## The account block's signed-out behaviour + +`us-scheduler/account` is the one block that can render **nothing**. It is meant +for a header, sidebar or account page, and its whole subject is the person +signed in — which a stranger is not. A bare "you are not signed in" in a site +header is noise that cannot be acted on, so: + +- **No login page chosen** → empty string for signed-out visitors. +- **Login page chosen** → a single **Sign in** link. + +Signed in, it shows the display name (`Auth\UserName::format()`, so a username +is never exposed), the account email, a **Sign out** link, and — only on an +account that books for someone other than itself — the students it books for. +Signing out returns to the chosen login page, or to the current page when there +is none, so a header sign-out does not also navigate the visitor somewhere. diff --git a/src/Auth/AccountPage.php b/src/Auth/AccountPage.php new file mode 100644 index 0000000..1ace546 --- /dev/null +++ b/src/Auth/AccountPage.php @@ -0,0 +1,91 @@ + $atts Block attributes (`loginPageId`) or + * shortcode attributes (`login_page_id`). + */ + public function render( array $atts ): string { + $loginPageId = Val::int( $atts['loginPageId'] ?? $atts['login_page_id'] ?? 0 ); + $loginUrl = $this->pageUrl( $loginPageId ); + + wp_enqueue_style( 'us-scheduler' ); + + if ( ! is_user_logged_in() ) { + if ( null === $loginUrl ) { + return ''; + } + + return sprintf( + '
', + esc_url( $loginUrl ), + esc_html__( 'Sign in', 'unsupervised-schedular' ) + ); + } + + // Always a WP_User here — is_user_logged_in() above rules out the + // id-0 placeholder wp_get_current_user() returns for a visitor. + $user = wp_get_current_user(); + + $name = UserName::format( $user, get_current_user_id() ); + $email = $user->user_email; + + // Whose lessons this account books, when that is more than just their own. + // A parent's first question on seeing "signed in as Grace" is whether this + // is the account their children's lessons are on. + $students = []; + foreach ( $this->guardians->bookableStudents( get_current_user_id() ) as $student ) { + if ( ! $student['is_self'] ) { + $students[] = $student['name']; + } + } + + // Back to where they were, so signing out of a header link does not also + // navigate them somewhere. The login page is the better landing spot when + // one is configured, since the current page may be members-only. + $logoutUrl = wp_logout_url( $loginUrl ?? (string) get_permalink() ); + + ob_start(); + include USC_PLUGIN_DIR . 'templates/frontend/account-page.php'; + return (string) ob_get_clean(); + } + + /** + * Permalink of a configured page, or null when none is chosen or the chosen + * page has since been deleted. + */ + private function pageUrl( int $pageId ): ?string { + if ( $pageId <= 0 ) { + return null; + } + + $url = get_permalink( $pageId ); + + return is_string( $url ) ? $url : null; + } +} diff --git a/src/BlockPreview.php b/src/BlockPreview.php index 47bce0d..9f5fd1d 100644 --- a/src/BlockPreview.php +++ b/src/BlockPreview.php @@ -210,6 +210,32 @@ class BlockPreview { ); } + /** + * Sample account panel. Shown populated whatever the editor's own login + * state, since on the published page a signed-out visitor may see nothing at + * all and an empty box tells the person placing the block nothing. + */ + public static function account(): string { + return sprintf( + '', + self::note( __( 'Editor preview — each visitor sees their own account here.', 'unsupervised-schedular' ) ), + esc_html__( 'Grace Hopper', 'unsupervised-schedular' ), + esc_html__( 'grace@example.com', 'unsupervised-schedular' ), + esc_html( + sprintf( + /* translators: %s: comma-separated list of the students this account books for. */ + __( 'Booking for %s', 'unsupervised-schedular' ), + __( 'Ada, Alan', 'unsupervised-schedular' ) + ) + ), + esc_html__( 'Sign out', 'unsupervised-schedular' ) + ); + } + private static function note( string $text ): string { return '' . esc_html( $text ) . '
'; } diff --git a/src/BlockRegistrar.php b/src/BlockRegistrar.php index f97c3c6..c59e1ff 100644 --- a/src/BlockRegistrar.php +++ b/src/BlockRegistrar.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace Unsupervised\Schedular; +use Unsupervised\Schedular\Auth\AccountPage; use Unsupervised\Schedular\Auth\LoginPage; use Unsupervised\Schedular\Auth\RegistrationPage; use Unsupervised\Schedular\Booking\BookingPage; @@ -30,6 +31,7 @@ class BlockRegistrar { private RegistrationPage $registrationPage, private GroupClassPage $groupClassPage, private FamilyPage $familyPage, + private AccountPage $accountPage, ) {} public function register(): void { @@ -148,6 +150,15 @@ class BlockRegistrar { ], ], ], + 'us-scheduler/account' => [ + 'render' => [ $this, 'renderAccount' ], + 'attributes' => [ + 'loginPageId' => [ + 'type' => 'number', + 'default' => 0, + ], + ], + ], ]; } @@ -195,6 +206,15 @@ class BlockRegistrar { return BlockPreview::groupClasses( Val::int( $attributes['offeringId'] ?? 0 ) > 0 ); } + /** + * Renders the account (who is signed in) block. + * + * @param array