(#3) - templating

This commit is contained in:
Martin Slachta
2026-06-14 07:16:13 +02:00
parent f4d3972d07
commit 8e264b8892
32 changed files with 870 additions and 126 deletions
+44 -42
View File
@@ -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);
+8
View File
@@ -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 -1
View File
@@ -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');
+14 -5
View File
@@ -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');
+44 -16
View File
@@ -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) {