#26 - Loading animation + success message fix

This commit was merged in pull request #31.
This commit is contained in:
Martin Slachta
2026-06-22 11:20:28 +02:00
parent c754e18a82
commit 97ee8fc991
32 changed files with 597 additions and 175 deletions
+10
View File
@@ -0,0 +1,10 @@
import { RsvTemplateRegistry } from './RsvTemplateRegistry.js';
import { RsvTemplateEngine } from './RsvTemplateEngine.js';
import { reservation_summary_renderer } from './elements/RsvReservationSummaryElement.js';
import { reset_form_button_renderer } from './elements/RsvResetFormButtonElement.js';
const registry = new RsvTemplateRegistry();
registry.register('reservation-summary', reservation_summary_renderer);
registry.register('reset-form-button', reset_form_button_renderer);
export const defaultEngine = new RsvTemplateEngine(registry);
+21
View File
@@ -0,0 +1,21 @@
/**
* Renders an array of slot objects into <li> DOM nodes for display in summary lists.
* Each slot must have start_utc, end_utc, price fields.
*/
export function render_slot_items(slots, locale, currency) {
const time_opts = { hour: '2-digit', minute: '2-digit' };
return slots.map(slot => {
const start = new Date(slot.start_utc);
const end = new Date(slot.end_utc);
const li = document.createElement('li');
li.className = 'rsv-summary-item';
li.innerHTML = `
<div class="rsv-summary-item-info">
<span class="rsv-summary-item-date">${start.toLocaleDateString(locale, { weekday: 'long', day: 'numeric', month: 'long' })}</span>
<span class="rsv-summary-item-time">${start.toLocaleTimeString(locale, time_opts)} ${end.toLocaleTimeString(locale, time_opts)}</span>
</div>
${slot.price > 0 ? `<span class="rsv-summary-item-price">${slot.price} ${currency}</span>` : ''}
`;
return li;
});
}
+94
View File
@@ -0,0 +1,94 @@
import { RsvTemplateRegistry } from './RsvTemplateRegistry.js';
function esc_html(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
export class RsvTemplateEngine {
constructor(registry = null) {
this.registry = registry ?? new RsvTemplateRegistry();
}
render(source, data = {}) {
const interpolated = this.interpolate(source, data);
return this.expand_elements(interpolated, data);
}
interpolate(source, data) {
return source.replace(/{{\s*([^}]+?)\s*}}/g, (match, path) => {
const value = this.resolve(path.trim(), data);
if (value === null || value === undefined || typeof value === 'object') {
return ''; // null and non-scalar (arrays, objects) render empty
}
return esc_html(String(value));
});
}
/**
*
* @param {*} data Submitted & computed data for the form
* @param {*} element The element to build the symbol table for
* @returns {Object} The symbol table for the element
*/
build_symbol_table(data, element) {
const attrs = {};
for (const attr of element.attributes) {
attrs[attr.name] = attr.value;
}
return { ...data, ...attrs };
}
wrap(element) {
const elWrap = document.createElement('div');
elWrap.innerHTML = element;
return elWrap;
}
expand_elements(html, data) {
html = html.replace(/<([\w-]+)([^>]*?)\/>/g, '<$1$2></$1>');
let doc = document.createElement('div');
doc.innerHTML = html;
[...doc.querySelectorAll('*')]
.filter(el => this.registry.has(el.tagName.toLowerCase()))
.forEach(el => {
const renderer = this.registry.get(el.tagName.toLowerCase());
el.replaceWith(this.wrap(renderer(this.build_symbol_table(data, el))));
});
const wrapper = document.createElement('div');
wrapper.appendChild(doc);
return wrapper.innerHTML;
}
resolve(path, data) {
const tokens = this.tokens(path);
let current = data;
for (const token of tokens) {
if (typeof current !== 'object' || current === null || !(token in current)) {
return null;
}
current = current[token];
}
return current;
}
tokens(path) {
const raw = path.split(/[\.\[\]]+/).filter(Boolean);
const result = [];
for (const token of raw) {
if (token === '$') continue;
result.push(token.replace(/^['"]|['"]$/g, ''));
}
return result;
}
}
@@ -0,0 +1,24 @@
/**
* Registry mapping custom element tag names to their renderer functions.
*/
export class RsvTemplateRegistry {
constructor() {
this.elements = new Map();
}
register(tag, renderer) {
this.elements.set(tag, renderer);
}
get(tag) {
return this.elements.get(tag) ?? null;
}
all() {
return this.elements;
}
has(tag) {
return this.elements.has(tag);
}
}
@@ -0,0 +1,57 @@
import { render_slot_items } from '../RsvSlotItems.js';
export function reservation_summary_renderer(symbols) {
const slots = symbols.slots ?? [];
const pricing = symbols.pricing ?? {};
if (!Array.isArray(slots) || slots.length === 0) {
return '';
}
const s = ReservairStrings.summary;
const locale = navigator.language;
const currency = pricing.currency ?? s.currency;
const items = render_slot_items(slots, locale, currency);
const itemsHtml = items.map(li => li.outerHTML).join('');
const footerRows = [];
const subtotal = pricing.subtotal ?? 0;
footerRows.push(`
<div class="rsv-summary-footer-row rsv-summary-subtotal">
<span class="rsv-summary-footer-label">${s.subtotal}</span>
<span class="rsv-summary-footer-value">${subtotal} ${currency}</span>
</div>
`);
if (pricing.discount && pricing.discount.percent > 0) {
const amount = pricing.discount.amount ?? 0;
const reason = pricing.discount.reason ?? '';
footerRows.push(`
<div class="rsv-summary-footer-row rsv-summary-discount">
<span class="rsv-summary-footer-label">${s.discount}: ${reason}</span>
<span class="rsv-summary-footer-value">-${amount} ${currency}</span>
</div>
`);
}
const total = pricing.total ?? 0;
footerRows.push(`
<div class="rsv-summary-footer-row rsv-summary-total">
<span class="rsv-summary-footer-label">${s.total}</span>
<span class="rsv-summary-footer-value">${total} ${currency}</span>
</div>
`);
return `
<div class="rsv-summary rsv-summary-snapshot">
<div class="rsv-summary-header">
<span class="rsv-summary-title">${s.title}</span>
</div>
<ul class="rsv-summary-list">${itemsHtml}</ul>
<div class="rsv-summary-footer rsv-summary-footer--pricing">
${footerRows.join('')}
</div>
</div>
`;
}
@@ -0,0 +1,13 @@
function esc_html(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
export function reset_form_button_renderer(symbols) {
const label = symbols.label ?? 'Odeslat znova';
return `<button type="button" class="rsv-form-btn" data-rsv-reset>${esc_html(label)}</button>`;
}