CI / Tests (PHP 8.2) (pull_request) Successful in 41s
CI / Tests (PHP 8.1) (pull_request) Successful in 42s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m48s
CI / Coding Standards (pull_request) Successful in 3m1s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m44s
CI / Build Plugin Zip (pull_request) Skipped
The block reports who is signed in and nothing more. Dropping the "Booking for …" line takes GuardianService with it — it was the only reason the page had a dependency at all, so AccountPage now constructs with no arguments. Co-Authored-By: Claude Opus 5 <[email protected]>
364 lines
18 KiB
JavaScript
364 lines
18 KiB
JavaScript
/* global wp */
|
||
(function () {
|
||
'use strict';
|
||
|
||
const { registerBlockType } = wp.blocks;
|
||
const { createElement: el, useState, useEffect } = wp.element;
|
||
const { useBlockProps, InspectorControls } = wp.blockEditor;
|
||
const { PanelBody, SelectControl, ToggleControl, TextareaControl } = wp.components;
|
||
const { useSelect } = wp.data;
|
||
const apiFetch = wp.apiFetch;
|
||
const ServerSideRender = wp.serverSideRender;
|
||
const { __ } = wp.i18n;
|
||
|
||
/**
|
||
* Dropdown of published pages with a leading "default" choice.
|
||
* Values are page IDs; 0 means the default behaviour.
|
||
*/
|
||
function PageSelect(props) {
|
||
const pages = useSelect(
|
||
(select) => select('core').getEntityRecords('postType', 'page', {
|
||
per_page: -1,
|
||
orderby: 'title',
|
||
order: 'asc',
|
||
status: 'publish',
|
||
_fields: 'id,title',
|
||
}),
|
||
[]
|
||
);
|
||
|
||
const options = [{ label: props.defaultLabel, value: '0' }].concat(
|
||
(pages || []).map((page) => ({
|
||
label: (page.title && page.title.rendered) || __('(no title)', 'unsupervised-schedular'),
|
||
value: String(page.id),
|
||
}))
|
||
);
|
||
|
||
return el(SelectControl, {
|
||
label: props.label,
|
||
help: props.help,
|
||
value: String(props.value || 0),
|
||
options: options,
|
||
onChange: (value) => props.onChange(parseInt(value, 10) || 0),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Dropdown of active group classes fetched from the plugin's public
|
||
* offerings endpoint. Values are offering IDs; 0 means all classes.
|
||
*/
|
||
function GroupClassSelect(props) {
|
||
const [offerings, setOfferings] = useState(null);
|
||
|
||
useEffect(() => {
|
||
apiFetch({ path: '/us-scheduler/v1/offerings?kind=group_class' })
|
||
.then(setOfferings)
|
||
.catch(() => setOfferings([]));
|
||
}, []);
|
||
|
||
const options = [{ label: __('All classes', 'unsupervised-schedular'), value: '0' }].concat(
|
||
(offerings || []).map((o) => ({
|
||
label: o.title || __('(no title)', 'unsupervised-schedular'),
|
||
value: String(o.id),
|
||
}))
|
||
);
|
||
|
||
// A previously chosen class that is no longer offered (deleted or
|
||
// deactivated) keeps its stored id visible instead of silently
|
||
// pretending "All classes" is selected.
|
||
const value = String(props.value || 0);
|
||
if (offerings !== null && !options.some((opt) => opt.value === value)) {
|
||
options.push({
|
||
label: __('Unavailable class #', 'unsupervised-schedular') + value,
|
||
value: value,
|
||
});
|
||
}
|
||
|
||
return el(SelectControl, {
|
||
label: props.label,
|
||
help: props.help,
|
||
value: value,
|
||
options: options,
|
||
onChange: (newValue) => props.onChange(parseInt(newValue, 10) || 0),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Dropdown of active private-lesson types fetched from the plugin's public
|
||
* offerings endpoint. Values are offering IDs; 0 means every type.
|
||
*/
|
||
function LessonTypeSelect(props) {
|
||
const [offerings, setOfferings] = useState(null);
|
||
|
||
useEffect(() => {
|
||
apiFetch({ path: '/us-scheduler/v1/offerings?kind=private_lesson' })
|
||
.then(setOfferings)
|
||
.catch(() => setOfferings([]));
|
||
}, []);
|
||
|
||
const options = [{ label: __('All lesson types', 'unsupervised-schedular'), value: '0' }].concat(
|
||
(offerings || []).map((o) => ({
|
||
label: o.duration_minutes
|
||
? `${o.title} (${o.duration_minutes} min)`
|
||
: (o.title || __('(no title)', 'unsupervised-schedular')),
|
||
value: String(o.id),
|
||
}))
|
||
);
|
||
|
||
// A previously chosen type that is no longer offered keeps its stored
|
||
// id visible instead of silently pretending "All lesson types" is set.
|
||
const value = String(props.value || 0);
|
||
if (offerings !== null && !options.some((opt) => opt.value === value)) {
|
||
options.push({
|
||
label: __('Unavailable lesson type #', 'unsupervised-schedular') + value,
|
||
value: value,
|
||
});
|
||
}
|
||
|
||
return el(SelectControl, {
|
||
label: props.label,
|
||
help: props.help,
|
||
value: value,
|
||
options: options,
|
||
onChange: (newValue) => props.onChange(parseInt(newValue, 10) || 0),
|
||
});
|
||
}
|
||
|
||
const blocks = [
|
||
{
|
||
name: 'us-scheduler/booking',
|
||
title: __('Lesson Booking', 'unsupervised-schedular'),
|
||
description: __('Lets students browse availability and book lessons. Shows a styled preview in the editor.', 'unsupervised-schedular'),
|
||
icon: 'calendar-alt',
|
||
keywords: ['booking', 'lesson', 'schedule'],
|
||
shortcode: 'us_booking',
|
||
attributes: {
|
||
loginPageId: { type: 'number', default: 0 },
|
||
autoRedirect: { type: 'boolean', default: false },
|
||
lessonTypeId: { type: 'number', default: 0 },
|
||
showTypeFilter: { type: 'boolean', default: true },
|
||
displayMode: { type: 'string', default: 'both' },
|
||
},
|
||
inspector: (attributes, setAttributes) => [
|
||
el(
|
||
PanelBody,
|
||
{ title: __('What to show', 'unsupervised-schedular'), key: 'display' },
|
||
el(SelectControl, {
|
||
label: __('Sections', 'unsupervised-schedular'),
|
||
help: __('Split the page in two: a booking calendar here, the student’s upcoming lessons somewhere else.', 'unsupervised-schedular'),
|
||
value: attributes.displayMode || 'both',
|
||
options: [
|
||
{ label: __('Booking and upcoming lessons', 'unsupervised-schedular'), value: 'both' },
|
||
{ label: __('Booking only', 'unsupervised-schedular'), value: 'booking' },
|
||
{ label: __('Upcoming lessons only', 'unsupervised-schedular'), value: 'upcoming' },
|
||
],
|
||
onChange: (displayMode) => setAttributes({ displayMode }),
|
||
})
|
||
),
|
||
el(
|
||
PanelBody,
|
||
{ title: __('Lesson types', 'unsupervised-schedular'), key: 'lesson-types' },
|
||
el(LessonTypeSelect, {
|
||
label: __('Lesson type', 'unsupervised-schedular'),
|
||
help: __('Show only the times bookable as one lesson type, for embedding on a page dedicated to it. That type is then the only one students can book here.', 'unsupervised-schedular'),
|
||
value: attributes.lessonTypeId,
|
||
onChange: (lessonTypeId) => setAttributes({ lessonTypeId }),
|
||
}),
|
||
el(ToggleControl, {
|
||
label: __('Show the lesson-type filter', 'unsupervised-schedular'),
|
||
help: __('Offer students the “Show Only” button that narrows the calendar to chosen lesson types. Not used when a single lesson type is set above.', 'unsupervised-schedular'),
|
||
checked: attributes.showTypeFilter !== false,
|
||
onChange: (showTypeFilter) => setAttributes({ showTypeFilter }),
|
||
})
|
||
),
|
||
el(
|
||
PanelBody,
|
||
{ title: __('Logged-out visitors', 'unsupervised-schedular'), key: 'logged-out' },
|
||
el(PageSelect, {
|
||
label: __('Login page', 'unsupervised-schedular'),
|
||
help: __('Where the log-in link sends visitors who are not logged in.', 'unsupervised-schedular'),
|
||
defaultLabel: __('WordPress login screen', 'unsupervised-schedular'),
|
||
value: attributes.loginPageId,
|
||
onChange: (loginPageId) => setAttributes({ loginPageId }),
|
||
}),
|
||
el(ToggleControl, {
|
||
label: __('Redirect automatically', 'unsupervised-schedular'),
|
||
help: __('Send logged-out visitors straight to the login page instead of showing a link.', 'unsupervised-schedular'),
|
||
checked: !!attributes.autoRedirect,
|
||
onChange: (autoRedirect) => setAttributes({ autoRedirect }),
|
||
})
|
||
),
|
||
],
|
||
},
|
||
{
|
||
name: 'us-scheduler/student-login',
|
||
title: __('Student Login', 'unsupervised-schedular'),
|
||
description: __('The front-end login form for students.', 'unsupervised-schedular'),
|
||
icon: 'admin-users',
|
||
keywords: ['login', 'student', 'sign in'],
|
||
shortcode: 'us_student_login',
|
||
attributes: {
|
||
bookingPageId: { type: 'number', default: 0 },
|
||
autoRedirect: { type: 'boolean', default: false },
|
||
},
|
||
inspector: (attributes, setAttributes) => el(
|
||
PanelBody,
|
||
{ title: __('Logged-in visitors', 'unsupervised-schedular') },
|
||
el(PageSelect, {
|
||
label: __('Booking page', 'unsupervised-schedular'),
|
||
help: __('Where students are sent after logging in, and where the link shown to already-logged-in visitors points.', 'unsupervised-schedular'),
|
||
defaultLabel: __('This page', 'unsupervised-schedular'),
|
||
value: attributes.bookingPageId,
|
||
onChange: (bookingPageId) => setAttributes({ bookingPageId }),
|
||
}),
|
||
el(ToggleControl, {
|
||
label: __('Redirect automatically', 'unsupervised-schedular'),
|
||
help: __('Send logged-in visitors straight to the booking page instead of showing a link. Requires a booking page to be chosen.', 'unsupervised-schedular'),
|
||
checked: !!attributes.autoRedirect,
|
||
onChange: (autoRedirect) => setAttributes({ autoRedirect }),
|
||
})
|
||
),
|
||
},
|
||
{
|
||
name: 'us-scheduler/student-register',
|
||
title: __('Student Registration', 'unsupervised-schedular'),
|
||
description: __('The invite-only student registration form.', 'unsupervised-schedular'),
|
||
icon: 'welcome-add-page',
|
||
keywords: ['register', 'student', 'invite'],
|
||
shortcode: 'us_student_register',
|
||
attributes: {
|
||
loginPageId: { type: 'number', default: 0 },
|
||
autoRedirect: { type: 'boolean', default: false },
|
||
inviteOnlyMessage: { type: 'string', default: '' },
|
||
},
|
||
inspector: (attributes, setAttributes) => [
|
||
el(
|
||
PanelBody,
|
||
{ title: __('After registration', 'unsupervised-schedular'), key: 'confirmation' },
|
||
el(PageSelect, {
|
||
label: __('Sign-in page', 'unsupervised-schedular'),
|
||
help: __('Where students are sent once registration finishes — after they confirm their email address, or straight away for an invited student.', 'unsupervised-schedular'),
|
||
defaultLabel: __('WordPress login screen', 'unsupervised-schedular'),
|
||
value: attributes.loginPageId,
|
||
onChange: (loginPageId) => setAttributes({ loginPageId }),
|
||
}),
|
||
el(ToggleControl, {
|
||
label: __('Redirect automatically', 'unsupervised-schedular'),
|
||
help: __('Send students straight to that page instead of showing the link. Requires a page to be chosen; errors and the "check your email" step are never skipped.', 'unsupervised-schedular'),
|
||
checked: !!attributes.autoRedirect,
|
||
onChange: (autoRedirect) => setAttributes({ autoRedirect }),
|
||
})
|
||
),
|
||
el(
|
||
PanelBody,
|
||
{ title: __('Invitation-only notice', 'unsupervised-schedular'), key: 'invite-only' },
|
||
el(TextareaControl, {
|
||
label: __('Message', 'unsupervised-schedular'),
|
||
help: __('Shown when registration is invite-only and the visitor has no valid invite link. Leave blank to use the default wording.', 'unsupervised-schedular'),
|
||
value: attributes.inviteOnlyMessage,
|
||
onChange: (inviteOnlyMessage) => setAttributes({ inviteOnlyMessage }),
|
||
})
|
||
),
|
||
],
|
||
},
|
||
{
|
||
name: 'us-scheduler/group-classes',
|
||
title: __('Group Classes', 'unsupervised-schedular'),
|
||
description: __('Lets students browse and enrol in group classes. Shows a styled preview in the editor.', 'unsupervised-schedular'),
|
||
icon: 'groups',
|
||
keywords: ['group', 'class', 'enrol'],
|
||
shortcode: 'us_group_classes',
|
||
attributes: {
|
||
offeringId: { type: 'number', default: 0 },
|
||
},
|
||
inspector: (attributes, setAttributes) => el(
|
||
PanelBody,
|
||
{ title: __('Classes shown', 'unsupervised-schedular') },
|
||
el(GroupClassSelect, {
|
||
label: __('Class', 'unsupervised-schedular'),
|
||
help: __('Show only one group class, for embedding on a page dedicated to it. That class’s description is left out — the card shows just the schedule, price and enrolment controls.', 'unsupervised-schedular'),
|
||
value: attributes.offeringId,
|
||
onChange: (offeringId) => setAttributes({ offeringId }),
|
||
})
|
||
),
|
||
},
|
||
{
|
||
name: 'us-scheduler/family',
|
||
title: __('Profile', 'unsupervised-schedular'),
|
||
description: __('Lets a parent or guardian add, edit and remove the students they book lessons for.', 'unsupervised-schedular'),
|
||
icon: 'groups',
|
||
// 'family' and 'children' are kept as search terms only — they are
|
||
// never displayed, and the block answered to them before it was
|
||
// renamed, so anyone reaching for the old word still finds it.
|
||
keywords: ['profile', 'students', 'family', 'children', 'guardian', 'parent'],
|
||
shortcode: 'us_family',
|
||
attributes: {
|
||
loginPageId: { type: 'number', default: 0 },
|
||
},
|
||
inspector: (attributes, setAttributes) => el(
|
||
PanelBody,
|
||
{ title: __('Logged-out visitors', 'unsupervised-schedular') },
|
||
el(PageSelect, {
|
||
label: __('Login page', 'unsupervised-schedular'),
|
||
help: __('Where visitors who are not signed in are sent to log in.', 'unsupervised-schedular'),
|
||
defaultLabel: __('WordPress login screen', 'unsupervised-schedular'),
|
||
value: attributes.loginPageId,
|
||
onChange: (loginPageId) => setAttributes({ loginPageId }),
|
||
})
|
||
),
|
||
},
|
||
{
|
||
name: 'us-scheduler/account',
|
||
title: __('Account', 'unsupervised-schedular'),
|
||
description: __('Shows the name and email of whoever is signed in, with 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) => {
|
||
registerBlockType(def.name, {
|
||
apiVersion: 3,
|
||
title: def.title,
|
||
description: def.description,
|
||
icon: def.icon,
|
||
category: 'widgets',
|
||
keywords: def.keywords,
|
||
supports: { html: false, multiple: false },
|
||
attributes: def.attributes || {},
|
||
example: {},
|
||
edit: function Edit(props) {
|
||
const inspector = def.inspector
|
||
? el(InspectorControls, {}, def.inspector(props.attributes, props.setAttributes))
|
||
: null;
|
||
|
||
return el(
|
||
'div',
|
||
useBlockProps(),
|
||
inspector,
|
||
el(ServerSideRender, { block: def.name, attributes: props.attributes })
|
||
);
|
||
},
|
||
save: () => null,
|
||
transforms: {
|
||
from: [{ type: 'shortcode', tag: def.shortcode }],
|
||
},
|
||
});
|
||
});
|
||
}());
|