Hoist Tailwind classes to CX object; fix no-slots overlay flash

- Move all Tailwind class strings to a module-level CX constant so the
  JIT scanner sees complete literals in one place rather than scattered
  across template expressions
- Convert no-slots overlay and wrapper from inline styles to CX entries
  (adds z-10 to fix stacking above FullCalendar grid)
- Fix no-availability message flashing on month navigation: reset
  fetchedRef in datesSet so eventsSet ignores stale pre-fetch firings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Philip Peterson 2026-06-03 21:20:04 -07:00
parent 1b7577fa17
commit 9e1e6a57b7
3 changed files with 93 additions and 52 deletions

View file

@ -635,6 +635,10 @@ html {
transform: translateY(-10px) rotate(-45deg); transform: translateY(-10px) rotate(-45deg);
} }
.pointer-events-none{
pointer-events: none;
}
.static{ .static{
position: static; position: static;
} }
@ -667,6 +671,10 @@ html {
top: 50%; top: 50%;
} }
.z-10{
z-index: 10;
}
.mx-auto{ .mx-auto{
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@ -1307,6 +1315,10 @@ html {
padding-top: 2rem; padding-top: 2rem;
} }
.pt-20{
padding-top: 5rem;
}
.text-left{ .text-left{
text-align: left; text-align: left;
} }

View file

@ -29,6 +29,48 @@ function formatPhone(raw) {
return "(" + d.slice(0, 3) + ") " + d.slice(3, 6) + "-" + d.slice(6); return "(" + d.slice(0, 3) + ") " + d.slice(3, 6) + "-" + d.slice(6);
} }
// All Tailwind class strings live here so the JIT scanner sees complete
// literals in one place rather than spread across template expressions.
const CX = {
// ── Type selector ──────────────────────────────────────────────────────
selectorLabel: "text-xs tracking-widest uppercase text-pt-blue-500 font-semibold mb-5",
selectorGrid: "grid grid-cols-1 sm:grid-cols-2 gap-3 mb-10",
typeBtn: "flex items-center gap-4 p-4 w-full rounded-xl border transition-colors",
typeBtnActive: "bg-pt-blue-500 border-pt-blue-500",
typeBtnInactive: "bg-white border-pt-blue-200 hover:border-pt-blue-500",
typeCircle: "w-8 h-8 rounded-full shrink-0 flex items-center justify-center border",
typeCircleActive: "border-white/60",
typeCircleInactive: "border-pt-blue-200",
typeLabel: "font-serif text-[1.0625rem] font-normal leading-snug",
typeLabelActive: "text-white",
typeLabelInactive: "text-gray-900",
typeDuration: "text-[0.6875rem] tracking-widest font-semibold mt-0.5",
typeDurationActive: "text-white/70",
typeDurationInactive: "text-pt-blue-500",
// ── Form ───────────────────────────────────────────────────────────────
formSection: "mt-8 pt-8 border-t border-pt-blue-200",
formHeading: "text-xs tracking-widest uppercase text-pt-blue-500 font-semibold mb-6",
formGrid: "grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-5 mb-5",
formLabel: "block text-sm font-medium text-gray-700 mb-1",
formRequired: "text-red-500",
formInput: "w-full border border-pt-blue-200 bg-white px-3 py-2 text-gray-900 text-sm focus:outline-none focus:border-pt-blue-500 transition-colors",
formTextarea: "resize-none w-full border border-pt-blue-200 bg-white px-3 py-2 text-gray-900 text-sm focus:outline-none focus:border-pt-blue-500 transition-colors",
formError: "text-red-500 text-sm mb-4",
submitBtn: "px-[4em] py-[1em] bg-pt-blue-500 text-white text-sm font-medium transition-colors border-2 border-pt-blue-500 hover:bg-pt-blue-600 hover:border-pt-blue-600 disabled:opacity-50",
// ── Calendar overlay ──────────────────────────────────────────────────
calWrapper: "relative",
noSlotsOverlay: "absolute inset-0 z-10 flex items-center justify-center pt-20 pointer-events-none",
// ── Success state ──────────────────────────────────────────────────────
successSection: "mt-8 pt-8 border-t border-pt-blue-200",
successBox: "p-6 bg-green-50 border border-green-200 text-green-800",
successTitle: "font-medium",
successBody: "text-sm mt-1",
successLink: "mt-3 text-sm text-green-700 underline hover:text-green-800",
};
var selectedDate = null; var selectedDate = null;
var selectedDateSlots = []; var selectedDateSlots = [];
@ -114,6 +156,7 @@ function Booking({ settings }) {
}); });
setSelectedSlotId(null); setSelectedSlotId(null);
setNoSlotsInMonth(false); setNoSlotsInMonth(false);
fetchedRef.current = false;
if (selectedDate) { if (selectedDate) {
var dayEl = calEl.current.querySelector(".fc-daygrid-day[data-date=\"" + selectedDate + "\"]"); var dayEl = calEl.current.querySelector(".fc-daygrid-day[data-date=\"" + selectedDate + "\"]");
if (dayEl) { if (dayEl) {
@ -263,13 +306,10 @@ function Booking({ settings }) {
var selectedSlot = slots.find(function (s) { return s.id === selectedSlotId; }); var selectedSlot = slots.find(function (s) { return s.id === selectedSlotId; });
var inputClass = "w-full border border-pt-blue-200 bg-white px-3 py-2 text-gray-900 text-sm focus:outline-none focus:border-pt-blue-500 transition-colors";
var labelClass = "block text-sm font-medium text-gray-700 mb-1";
return html` return html`
<div> <div>
<p class="text-xs tracking-widest uppercase text-pt-blue-500 font-semibold mb-5">Select Appointment Type</p> <p class=${CX.selectorLabel}>Select Appointment Type</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-10"> <div class=${CX.selectorGrid}>
${TYPES.map(function (t) { ${TYPES.map(function (t) {
var active = service === t.id; var active = service === t.id;
return html` return html`
@ -277,22 +317,16 @@ function Booking({ settings }) {
key=${t.id} key=${t.id}
onClick=${function () { setService(t.id); }} onClick=${function () { setService(t.id); }}
style="text-align:left; cursor:pointer;" style="text-align:left; cursor:pointer;"
class=${ class=${CX.typeBtn + " " + (active ? CX.typeBtnActive : CX.typeBtnInactive)}
"flex items-center gap-4 p-4 w-full rounded-xl border transition-colors " +
(active ? "bg-pt-blue-500 border-pt-blue-500" : "bg-white border-pt-blue-200 hover:border-pt-blue-500")
}
> >
<div class=${ <div class=${CX.typeCircle + " " + (active ? CX.typeCircleActive : CX.typeCircleInactive)}>
"w-8 h-8 rounded-full shrink-0 flex items-center justify-center border " +
(active ? "border-white/60" : "border-pt-blue-200")
}>
${active ? CHECK : null} ${active ? CHECK : null}
</div> </div>
<div> <div>
<p class=${"font-serif text-[1.0625rem] font-normal leading-snug " + (active ? "text-white" : "text-gray-900")}> <p class=${CX.typeLabel + " " + (active ? CX.typeLabelActive : CX.typeLabelInactive)}>
${t.label} ${t.label}
</p> </p>
<p class=${"text-[0.6875rem] tracking-widest font-semibold mt-0.5 " + (active ? "text-white/70" : "text-pt-blue-500")}> <p class=${CX.typeDuration + " " + (active ? CX.typeDurationActive : CX.typeDurationInactive)}>
${t.duration} ${t.duration}
</p> </p>
</div> </div>
@ -302,10 +336,10 @@ function Booking({ settings }) {
</div> </div>
<div class="riverside-booking-wrap"> <div class="riverside-booking-wrap">
<div style="position:relative"> <div class=${CX.calWrapper}>
<div ref=${calEl} id="riverside-calendar"></div> <div ref=${calEl} id="riverside-calendar"></div>
${noSlotsInMonth ? html` ${noSlotsInMonth ? html`
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding-top:5rem;pointer-events:none;"> <div class=${CX.noSlotsOverlay}>
<p style="font-size:0.875rem;color:#6b7280;border:1px solid #b8d4dc;background:#fff;padding:0.5rem 1rem;"> <p style="font-size:0.875rem;color:#6b7280;border:1px solid #b8d4dc;background:#fff;padding:0.5rem 1rem;">
No availability this month No availability this month
</p> </p>
@ -331,10 +365,10 @@ function Booking({ settings }) {
</div> </div>
${success ? html` ${success ? html`
<div class="mt-8 pt-8 border-t border-pt-blue-200"> <div class=${CX.successSection}>
<div class="p-6 bg-green-50 border border-green-200 text-green-800"> <div class=${CX.successBox}>
<p class="font-medium">Request received!</p> <p class=${CX.successTitle}>Request received!</p>
<p class="text-sm mt-1">Thank you. We'll contact you shortly to confirm your appointment.</p> <p class=${CX.successBody}>Thank you. We'll contact you shortly to confirm your appointment.</p>
<button <button
type="button" type="button"
onClick=${function () { onClick=${function () {
@ -346,82 +380,78 @@ function Booking({ settings }) {
}); });
} }
}} }}
class="mt-3 text-sm text-green-700 underline hover:text-green-800" class=${CX.successLink}
>Book another appointment</button> >Book another appointment</button>
</div> </div>
</div> </div>
` : null} ` : null}
${selectedSlot ? html` ${selectedSlot ? html`
<form <form onSubmit=${handleSubmit} autocomplete="on" class=${CX.formSection}>
onSubmit=${handleSubmit} <p class=${CX.formHeading}>Your Details</p>
class="mt-8 pt-8 border-t border-pt-blue-200"
>
<p class="text-xs tracking-widest uppercase text-pt-blue-500 font-semibold mb-6">
Your Details
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-5 mb-5"> <div class=${CX.formGrid}>
<div> <div>
<label class=${labelClass}> <label class=${CX.formLabel}>
First name <span class="text-red-500">*</span> First name <span class=${CX.formRequired}>*</span>
</label> </label>
<input <input
type="text" type="text"
name="first_name"
autocomplete="given-name"
required required
value=${formData.firstName} value=${formData.firstName}
onInput=${function (e) { handleFormChange("firstName", e.target.value); }} onInput=${function (e) { handleFormChange("firstName", e.target.value); }}
class=${inputClass} class=${CX.formInput}
/> />
</div> </div>
<div> <div>
<label class=${labelClass}> <label class=${CX.formLabel}>
Last name <span class="text-red-500">*</span> Last name <span class=${CX.formRequired}>*</span>
</label> </label>
<input <input
type="text" type="text"
name="last_name"
autocomplete="family-name"
required required
value=${formData.lastName} value=${formData.lastName}
onInput=${function (e) { handleFormChange("lastName", e.target.value); }} onInput=${function (e) { handleFormChange("lastName", e.target.value); }}
class=${inputClass} class=${CX.formInput}
/> />
</div> </div>
<div class="sm:col-span-2"> <div class="sm:col-span-2">
<label class=${labelClass}> <label class=${CX.formLabel}>
Phone number <span class="text-red-500">*</span> Phone number <span class=${CX.formRequired}>*</span>
</label> </label>
<input <input
type="tel" type="tel"
name="phone"
autocomplete="tel"
required required
value=${formatPhone(formData.phone)} value=${formatPhone(formData.phone)}
onInput=${function (e) { onInput=${function (e) {
// Compute from the tentative input value (supports free typing/paste/backspace anywhere). handleFormChange("phone", formatPhone(e.target.value));
// We store the formatted result so the email and prefill see it nicely displayed.
const next = formatPhone(e.target.value);
handleFormChange("phone", next);
}} }}
class=${inputClass} class=${CX.formInput}
/> />
</div> </div>
</div> </div>
<div class="mb-6"> <div class="mb-6">
<label class=${labelClass}>Comments</label> <label class=${CX.formLabel}>Comments</label>
<textarea <textarea
rows="4" rows="4"
name="comments"
autocomplete="off"
value=${formData.comments} value=${formData.comments}
onInput=${function (e) { handleFormChange("comments", e.target.value); }} onInput=${function (e) { handleFormChange("comments", e.target.value); }}
class=${"resize-none " + inputClass} class=${CX.formTextarea}
></textarea> ></textarea>
</div> </div>
${submitError ? html`<p class="text-red-500 text-sm mb-4">${submitError}</p>` : null} ${submitError ? html`<p class=${CX.formError}>${submitError}</p>` : null}
<button <button type="submit" disabled=${submitting} class=${CX.submitBtn}>
type="submit"
disabled=${submitting}
class="px-[4em] py-[1em] bg-pt-blue-500 text-white text-sm font-medium transition-colors border-2 border-pt-blue-500 hover:bg-pt-blue-600 hover:border-pt-blue-600 disabled:opacity-50"
>
${submitting ? "Submitting…" : "Request appointment"} ${submitting ? "Submitting…" : "Request appointment"}
</button> </button>
</form> </form>

View file

@ -14,7 +14,6 @@ use Symfony\Component\HttpFoundation\Request;
class ScheduleController extends ControllerBase { class ScheduleController extends ControllerBase {
private PrivateTempStore $tempStore; private PrivateTempStore $tempStore;
private $configFactory;
public function __construct( public function __construct(
PrivateTempStoreFactory $tempStoreFactory, PrivateTempStoreFactory $tempStoreFactory,