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:
parent
1b7577fa17
commit
9e1e6a57b7
3 changed files with 93 additions and 52 deletions
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue