Files
unsupervised-scheduler/assets/js/blocks.js
T
thatguygriffandClaude Opus 5 264d9cba01
CI / Tests (PHP 8.1) (pull_request) Successful in 47s
CI / Tests (PHP 8.2) (pull_request) Successful in 54s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m51s
CI / Coding Standards (pull_request) Successful in 2m56s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m42s
CI / Build Plugin Zip (pull_request) Skipped
Add per-embed lesson-type and section options to the booking block
Three sidebar options on the Lesson Booking block, all mirrored as shortcode
attributes and carried to the front end as data attributes on
#us-booking-app (or as omitted containers):

- Lesson type (lessonTypeId / lesson_type) pins the calendar to a single
  private-lesson type: only the times bookable as it are listed, and it is
  the only type bookable there, auto-selected on the registration form. A
  pinned type that is no longer offered says so instead of showing an empty
  calendar.
- Show the lesson-type filter (showTypeFilter / show_filter) drops the
  "Show Only" control for studios that do not want it.
- Sections (displayMode / show) embeds one half of the page — the booking
  calendar or the student's upcoming lessons — so the two can live on
  different pages. The script skips the work belonging to a missing half:
  no availability or catalog request for an upcoming-only embed, no
  bookings request for a booking-only one. An unrecognised value renders
  the whole page. The editor preview follows the same setting.

Also fixes the expanded filter's first lesson type sharing a line with the
"Lesson type" heading — the choices now sit in their own row beneath it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-28 13:16:18 -03:00

317 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 students 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 classs description is left out — the card shows just the schedule, price and enrolment controls.', 'unsupervised-schedular'),
value: attributes.offeringId,
onChange: (offeringId) => setAttributes({ offeringId }),
})
),
},
];
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 }],
},
});
});
}());