initial
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
class RsvTimeline extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['timetable-id', 'date'];
|
||||
}
|
||||
|
||||
// ---- Attribute accessors ------------------------------------------------
|
||||
|
||||
get timetableId() { return parseInt(this.getAttribute('timetable-id')); }
|
||||
|
||||
get date() {
|
||||
const attr = this.getAttribute('date');
|
||||
// Parse as local midnight so setHours() in block rendering stays in local time.
|
||||
return attr ? new Date(attr + 'T12:00:00') : new Date();
|
||||
}
|
||||
|
||||
set date(value) {
|
||||
const d = new Date(value);
|
||||
const str = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
this.setAttribute('date', str);
|
||||
}
|
||||
|
||||
// ---- Lifecycle ----------------------------------------------------------
|
||||
|
||||
connectedCallback() {
|
||||
this._version = 0;
|
||||
this.classList.add('rsv-slots-list');
|
||||
this.addEventListener('click', this._on_click.bind(this));
|
||||
this._render();
|
||||
}
|
||||
|
||||
attributeChangedCallback(_attr, oldVal, newVal) {
|
||||
if (oldVal === newVal || !this.isConnected) return;
|
||||
this._render();
|
||||
}
|
||||
|
||||
// ---- Private ------------------------------------------------------------
|
||||
|
||||
_on_click(event) {
|
||||
const slot = event.target.closest('.rsv-slots-slot');
|
||||
if (slot && !slot.classList.contains('rsv-slots-slot-full')) {
|
||||
slot.classList.toggle('rsv-slots-slot-selected');
|
||||
slot.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
async _render() {
|
||||
// Version guard: discard renders that were superseded by a newer call.
|
||||
const v = ++this._version;
|
||||
const s = ReservairStrings.timeline;
|
||||
|
||||
if (this.timetableId === null) {
|
||||
this.replaceChildren(this._notice(s.not_reservable));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
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');
|
||||
header.textContent = this.date.toLocaleDateString(navigator.language, {
|
||||
weekday: 'long', day: 'numeric', month: 'long',
|
||||
}).replace(',', '');
|
||||
|
||||
const blocks = [];
|
||||
|
||||
for (const { from_minutes, to_minutes, block_size_in_minutes, occupancy: block_occ } of occupancy) {
|
||||
if (from_minutes === to_minutes || block_occ.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const from_block = parseInt(from_minutes) / block_size_in_minutes;
|
||||
|
||||
const time_slots = block_occ.map((occ, i) =>
|
||||
this._block(this.date, occ, block_size_in_minutes, from_block + i)
|
||||
);
|
||||
|
||||
const time_slot_group = document.createElement('div');
|
||||
time_slot_group.classList.add('rsv-slots-group');
|
||||
time_slot_group.replaceChildren(...time_slots);
|
||||
blocks.push(time_slot_group);
|
||||
}
|
||||
|
||||
this.replaceChildren(header, ...blocks);
|
||||
} catch (_e) {
|
||||
if (v !== this._version) return;
|
||||
this.replaceChildren(this._notice(s.no_blocks));
|
||||
}
|
||||
}
|
||||
|
||||
_block(date, left, block_size, idx) {
|
||||
const from = new Date(date);
|
||||
from.setHours(0, idx * block_size, 0, 0);
|
||||
|
||||
const to = new Date(from);
|
||||
to.setMinutes(to.getMinutes() + block_size);
|
||||
|
||||
const cell = document.createElement('div');
|
||||
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');
|
||||
|
||||
const time_el = document.createElement('span');
|
||||
time_el.classList.add('rsv-slots-slot-time');
|
||||
time_el.textContent = `${this._fmt(from)} – ${this._fmt(to)}`;
|
||||
|
||||
const badge = document.createElement('span');
|
||||
badge.classList.add('rsv-slots-slot-badge');
|
||||
const remaining_seats = left;
|
||||
|
||||
if (remaining_seats > 0) badge.classList.add('rsv-slots-slot-badge-available');
|
||||
|
||||
if (remaining_seats === 1) badge.textContent = `${remaining_seats} místo`;
|
||||
else if (remaining_seats >= 2 && remaining_seats <= 4) badge.textContent = `${remaining_seats} místa`;
|
||||
else badge.textContent = `${remaining_seats} míst`;
|
||||
|
||||
|
||||
cell.append(time_el, badge);
|
||||
return cell;
|
||||
}
|
||||
|
||||
_notice(text) {
|
||||
const p = document.createElement('p');
|
||||
p.classList.add('rsv-slots-notice');
|
||||
p.textContent = text;
|
||||
return p;
|
||||
}
|
||||
|
||||
_fmt(dt) {
|
||||
return dt.getHours() + ':' + String(dt.getMinutes()).padStart(2, '0');
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('rsv-timeline', RsvTimeline);
|
||||
Reference in New Issue
Block a user