(#3) - templating
This commit is contained in:
@@ -1,19 +1,30 @@
|
||||
.rsv-form-btn {
|
||||
border-radius: 1.375rem;
|
||||
border: 1.5px solid #e0e0e0;
|
||||
color: #555;
|
||||
padding: 0 calc(1.25rem + 4px);
|
||||
height: 3.5rem;
|
||||
background-color: white;
|
||||
|
||||
line-height: 140%;
|
||||
font-size: 1rem;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.rsv-form-btn:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Primary CTA (submit / confirm) */
|
||||
.rsv-form-btn-primary {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
|
||||
border-radius: 1.375rem;
|
||||
font-size: 1rem;
|
||||
padding: 0 calc(1.25rem + 4px);
|
||||
line-height: 140%;
|
||||
height: 3.5rem;
|
||||
|
||||
|
||||
border: none;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
transition: background .12s;
|
||||
@@ -43,12 +54,6 @@
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/*.reservair-form button {
|
||||
padding: var(--s-3) !important;
|
||||
font-weight: 400 !important;
|
||||
}*/
|
||||
|
||||
/*.reservair-form button,*/
|
||||
.rsv-form-input {
|
||||
border: 1px solid var(--color-gray-300);
|
||||
outline: none;
|
||||
@@ -165,47 +170,44 @@
|
||||
box-shadow: 0 0 0 4px color-mix(in oklab, var(--color-red-500) 25%, transparent) !important;
|
||||
}
|
||||
|
||||
.rsv-success-message {
|
||||
.rsv-success-msg {
|
||||
text-align: center;
|
||||
padding: var(--s-5);
|
||||
color: var(--color-green-700);
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.rsv-success-message p {
|
||||
.rsv-success-msg p {
|
||||
margin-top: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
.mesg {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
line-height: 1rem;
|
||||
margin-top: var(--s-5);
|
||||
margin-bottom: var(--s-5);
|
||||
padding: var(--s-4) 0;
|
||||
.rsv-success-msg h1,
|
||||
.rsv-success-msg h2,
|
||||
.rsv-success-msg h3,
|
||||
.rsv-success-msg h4,
|
||||
.rsv-success-msg h5,
|
||||
.rsv-success-msg h6
|
||||
{
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.success-mesg-icon {
|
||||
.rsv-summary {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.mesg-icon svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: var(--s-2);
|
||||
.rsv-success-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
color: #00000094;
|
||||
background: #dcfce7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.error-mesg svg {
|
||||
background-color: var(--color-red-200);
|
||||
}
|
||||
|
||||
.success-mesg svg {
|
||||
background-color: rgba(0, 201, 80, 0.36);
|
||||
}
|
||||
/* FORM END */
|
||||
|
||||
.rsv-timetable-selector {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
@@ -179,6 +179,14 @@ export const RsvCalendarPicker = (() => {
|
||||
new InputEvent('change', { bubbles: true, cancelable: true, composed: true })
|
||||
);
|
||||
},
|
||||
|
||||
// Re-check the radio for the current date. The date lives in JS state, so
|
||||
// this restores the visual selection after a native form.reset() clears it.
|
||||
reselect() {
|
||||
if (this.date === null) return;
|
||||
const radio = this.body.querySelector(`input[id="${this.date.toISOString()}"]`);
|
||||
if (radio) radio.checked = true;
|
||||
},
|
||||
};
|
||||
|
||||
container.classList.add('rsv-calendar');
|
||||
|
||||
@@ -37,6 +37,16 @@ class RsvReservationSelector extends HTMLElement {
|
||||
this.querySelectorAll('.rsv-slots-slot-selected').forEach(s => s.classList.remove('rsv-slots-slot-selected'));
|
||||
this._slots = [];
|
||||
this._commit();
|
||||
// _commit clears only the slots; keep the picked date selected, re-asserting
|
||||
// it in case a surrounding form.reset() just unchecked the calendar radio.
|
||||
this._calendar?.reselect();
|
||||
}
|
||||
|
||||
// Reset for a new reservation: drop the local selection (keeping the date) and
|
||||
// reload availability so slots booked by the previous submission show as full.
|
||||
reset() {
|
||||
this.clear();
|
||||
this.querySelector('rsv-timeline')?.refresh();
|
||||
}
|
||||
|
||||
// ---- Private ------------------------------------------------------------
|
||||
|
||||
@@ -25,6 +25,31 @@ class RsvReservationSummary extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Public API ---------------------------------------------------------
|
||||
|
||||
// Detached, static copy of the current selection for the success message.
|
||||
// Mirrors the live layout minus the interactive "clear all" control.
|
||||
snapshot() {
|
||||
const s = ReservairStrings.summary;
|
||||
const list = this.querySelector('.rsv-summary-list');
|
||||
const count = this.querySelector('.rsv-summary-count');
|
||||
const price = this.querySelector('.rsv-summary-price');
|
||||
|
||||
const node = document.createElement('div');
|
||||
node.className = 'rsv-summary rsv-summary-snapshot';
|
||||
node.innerHTML = `
|
||||
<div class="rsv-summary-header">
|
||||
<span class="rsv-summary-title">${s.title}</span>
|
||||
</div>
|
||||
<ul class="rsv-summary-list">${list ? list.innerHTML : ''}</ul>
|
||||
<div class="rsv-summary-footer">
|
||||
<span class="rsv-summary-count">${count ? count.textContent : ''}</span>
|
||||
<div class="rsv-summary-price">${price ? price.textContent : ''}</div>
|
||||
</div>
|
||||
`;
|
||||
return node;
|
||||
}
|
||||
|
||||
// ---- Private ------------------------------------------------------------
|
||||
|
||||
_build() {
|
||||
@@ -51,7 +76,6 @@ class RsvReservationSummary extends HTMLElement {
|
||||
const all_slots = [...this._all_slots.values()].flatMap(({ slots, price_per_block }) =>
|
||||
slots.map(s => ({ ...s, price_per_block }))
|
||||
);
|
||||
console.log(all_slots);
|
||||
|
||||
const n = all_slots.length;
|
||||
const list = this.querySelector('.rsv-summary-list');
|
||||
|
||||
@@ -35,6 +35,14 @@ class RsvTimeline extends HTMLElement {
|
||||
this._render();
|
||||
}
|
||||
|
||||
// ---- Public API ---------------------------------------------------------
|
||||
|
||||
// Re-fetch availability for the current date, e.g. after a booking occupied
|
||||
// some slots. The date is unchanged, so attributeChangedCallback won't fire.
|
||||
refresh() {
|
||||
this._render();
|
||||
}
|
||||
|
||||
// ---- Private ------------------------------------------------------------
|
||||
|
||||
_on_click(event) {
|
||||
@@ -59,10 +67,6 @@ class RsvTimeline extends HTMLElement {
|
||||
const occupancy = await RsvTimetableService.get_availability_for_date(this.timetableId, this.date);
|
||||
if (v !== this._version) return;
|
||||
|
||||
if(occupancy.length === 0) {
|
||||
this.replaceChildren(this._notice(s.no_blocks));
|
||||
return;
|
||||
}
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.classList.add('rsv-slots-label');
|
||||
@@ -70,6 +74,11 @@ class RsvTimeline extends HTMLElement {
|
||||
weekday: 'long', day: 'numeric', month: 'long',
|
||||
}).replace(',', '');
|
||||
|
||||
if(occupancy.length === 0) {
|
||||
this.replaceChildren(header, this._notice(s.no_blocks));
|
||||
return;
|
||||
}
|
||||
|
||||
const blocks = [];
|
||||
|
||||
for (const { from_minutes, to_minutes, block_size_in_minutes, occupancy: block_occ } of occupancy) {
|
||||
@@ -107,7 +116,7 @@ class RsvTimeline extends HTMLElement {
|
||||
cell.classList.add('rsv-slots-slot', 'rsv-slots-slot-available');
|
||||
cell.dataset.start_utc = from.toISOString();
|
||||
cell.dataset.end_utc = to.toISOString();
|
||||
if (left === 0) cell.classList.add('rsv-slots-slot-full');
|
||||
if (left <= 0) cell.classList.add('rsv-slots-slot-full');
|
||||
|
||||
const time_el = document.createElement('span');
|
||||
time_el.classList.add('rsv-slots-slot-time');
|
||||
|
||||
@@ -56,24 +56,14 @@ export const RsvFormSender = {
|
||||
svg.appendChild(path);
|
||||
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'success-icon';
|
||||
icon.className = 'rsv-success-icon';
|
||||
icon.appendChild(svg);
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'success-title';
|
||||
title.textContent = s.success_title;
|
||||
|
||||
const subtitle = document.createElement('p');
|
||||
subtitle.className = 'success-msg';
|
||||
subtitle.textContent = s.success_subtitle;
|
||||
|
||||
const reset_btn = document.createElement('button');
|
||||
reset_btn.className = 'reset-btn';
|
||||
reset_btn.textContent = s.new_reservation;
|
||||
const body = this.build_success_body(form, s);
|
||||
|
||||
const state = document.createElement('div');
|
||||
state.className = 'success-state';
|
||||
state.append(icon, title, subtitle, reset_btn);
|
||||
state.className = 'rsv-success-state';
|
||||
state.append(icon, body);
|
||||
|
||||
const msg = document.createElement('div');
|
||||
msg.appendChild(state);
|
||||
@@ -81,12 +71,50 @@ export const RsvFormSender = {
|
||||
existing.forEach(child => child.style.display = 'none');
|
||||
wrapper.appendChild(msg);
|
||||
|
||||
reset_btn.addEventListener('click', () => {
|
||||
// The form catches every data-rsv-reset button in the card and links it to
|
||||
// its cleanup — so new buttons just need the marker, no wiring here.
|
||||
const reset = () => {
|
||||
msg.remove();
|
||||
form.reset();
|
||||
// Native reset leaves custom controls untouched; reset the reservation
|
||||
// selectors so their slots, hidden inputs and the summary clear, the date
|
||||
// stays selected and availability reloads with the just-booked slots.
|
||||
form.querySelectorAll('rsv-reservation-selector').forEach(sel => sel.reset());
|
||||
this.clear_feedback(form);
|
||||
existing.forEach(child => child.style.display = '');
|
||||
});
|
||||
};
|
||||
state.querySelectorAll('[data-rsv-reset]').forEach(btn => btn.addEventListener('click', reset));
|
||||
},
|
||||
|
||||
// Body of the success card. Uses the admin-configured template when the form
|
||||
// ships one, filling the .rsv-success-summary placeholder (expanded server-side
|
||||
// from <reservation-summary>) with a snapshot of the selected slots; otherwise
|
||||
// falls back to the default text.
|
||||
build_success_body(form, strings) {
|
||||
const tpl = form.parentElement?.querySelector('template.rsv-form-success');
|
||||
|
||||
if (!tpl) {
|
||||
const subtitle = document.createElement('p');
|
||||
subtitle.className = 'rsv-success-msg';
|
||||
subtitle.textContent = strings.success_subtitle;
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'rsv-success-msg';
|
||||
body.appendChild(tpl.content.cloneNode(true));
|
||||
|
||||
const placeholder = body.querySelector('.rsv-success-summary');
|
||||
if (placeholder) {
|
||||
const summary = form.querySelector('rsv-reservation-summary');
|
||||
if (summary && typeof summary.snapshot === 'function') {
|
||||
placeholder.replaceWith(summary.snapshot());
|
||||
} else {
|
||||
placeholder.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return body;
|
||||
},
|
||||
|
||||
set_loading(form, is_loading) {
|
||||
|
||||
Reference in New Issue
Block a user