Przejdź do treści
<code>pen.pl

Rejestr kluczy — recepcja motelu

Narzędzia

Podgląd

Ładowanie podglądu…

Recepcyjna księga kluczy z filtrem statusu, sortowaniem kolumn i podziałem pokoi na strony.

0,0 / 5 Brak ocen

Kod źródłowy

Licencja MIT — także do użytku komercyjnego. Zachowaj informacje o autorze i licencji.

HTML
<main class="motel-board" id="motel-table"><header><div class="star" aria-hidden="true">✦</div><div><p>MOTEL SŁOŃCE · RECEPCJA</p><h1>Rejestr kluczy</h1></div><div class="shift">ZMIANA<br><b>PM</b></div></header>
  <section class="desk" aria-label="Filtry kluczy"><label>Pokój, skrzydło lub numer klucza<input type="search" data-search placeholder="np. 204 albo Patio"></label><fieldset><legend>Status klucza</legend><label><input type="radio" name="motel-status" value="" data-filter checked> Wszystkie</label><label><input type="radio" name="motel-status" value="Dostępny" data-filter> Dostępne</label><label><input type="radio" name="motel-status" value="Wydany" data-filter> Wydane</label><label><input type="radio" name="motel-status" value="Serwis" data-filter> Serwis</label></fieldset><button type="button" data-reset>Wyczyść</button></section>
  <div class="motel-summary"><p role="status" aria-live="polite"></p><span>13 PAŹ · 19:45</span></div><div class="table-scroll" tabindex="0" aria-label="Tabela kluczy motelowych — przewijaj poziomo na małym ekranie"><table><caption>Księga wydania kluczy</caption><thead><tr>
    <th scope="col"><button type="button" data-sort="room">Pokój <span data-sort-mark aria-hidden="true">↕</span></button></th><th scope="col"><button type="button" data-sort="wing">Skrzydło <span data-sort-mark aria-hidden="true">↕</span></button></th><th scope="col"><button type="button" data-sort="key">Klucz <span data-sort-mark aria-hidden="true">↕</span></button></th><th scope="col"><button type="button" data-sort="guest">Gość <span data-sort-mark aria-hidden="true">↕</span></button></th><th scope="col"><button type="button" data-sort="status">Status <span data-sort-mark aria-hidden="true">↕</span></button></th><th scope="col"><button type="button" data-sort="return">Zwrot <span data-sort-mark aria-hidden="true">↕</span></button></th>
  </tr></thead><tbody><tr><td colspan="6">Otwieranie księgi…</td></tr></tbody></table></div><nav class="motel-pages" data-pages aria-label="Stronicowanie tabeli"></nav>
</main>
CSS
*{box-sizing:border-box}body{margin:0;background:#173c3b;color:#27302c;font-family:"Arial Narrow",Arial,sans-serif}.motel-board{min-height:100vh;padding:clamp(16px,4vw,46px);background:#efce98;border:12px solid #173c3b}.motel-board>header{display:flex;align-items:center;gap:18px;border-bottom:5px double #9a2d25;padding-bottom:15px}.motel-board header p{margin:0;color:#9a2d25;font-size:12px;font-weight:800;letter-spacing:.15em}.motel-board h1{margin:2px 0 0;color:#173c3b;font:900 clamp(37px,7vw,78px)/.85 Georgia,serif;letter-spacing:-.055em}.star{width:64px;height:64px;display:grid;place-items:center;border:3px solid #9a2d25;color:#9a2d25;font-size:38px}.shift{margin-left:auto;border-left:2px solid #173c3b;padding-left:16px;font-size:10px;letter-spacing:.14em}.shift b{font-size:30px}.desk{display:grid;grid-template-columns:minmax(210px,.7fr) minmax(420px,1.3fr) auto;align-items:end;gap:16px;padding:20px 0 12px}.desk>label{display:grid;gap:6px;font-size:12px;font-weight:800;text-transform:uppercase}.desk input[type="search"]{height:46px;border:2px solid #173c3b;border-radius:0;background:#fff5de;color:#27302c;padding:0 11px;font:inherit}.desk fieldset{min-height:70px;border:2px solid #173c3b;padding:8px 10px}.desk legend{padding:0 5px;font-size:11px;font-weight:800;text-transform:uppercase}.desk fieldset label{display:inline-flex;align-items:center;min-height:44px;margin-right:10px;white-space:nowrap}.desk input[type="radio"]{width:18px;height:18px;accent-color:#9a2d25}.desk>button{height:46px;border:2px solid #173c3b;border-radius:0;background:#9a2d25;color:#fff6df;font-weight:800;text-transform:uppercase;cursor:pointer;padding:0 18px}.motel-summary{display:flex;justify-content:space-between;font-size:12px;font-weight:800;letter-spacing:.06em}.motel-summary p{margin:7px 0}.table-scroll{max-width:100%;overflow-x:auto;border:3px solid #173c3b;background:#fff5de}.table-scroll:focus-visible{outline:4px solid #9a2d25;outline-offset:3px}table{width:100%;min-width:760px;border-collapse:collapse}caption{text-align:left;padding:12px 14px;background:#173c3b;color:#fff5de;font:700 18px Georgia,serif}th,td{text-align:left;border-bottom:1px solid #999079;padding:10px 12px}thead{background:#dbb675}thead th{padding:0 10px}th button{min-height:48px;width:100%;border:0;background:transparent;text-align:left;color:#173c3b;font-weight:900;text-transform:uppercase;cursor:pointer}th[aria-sort]{background:#9a2d25}th[aria-sort] button{color:#fff5de}tbody th{font:900 24px Georgia,serif;color:#9a2d25}.key-tag{display:inline-block;min-width:62px;border:2px solid #173c3b;padding:5px 7px;text-align:center;font-weight:900}.state{font-weight:900;text-transform:uppercase;font-size:12px}.state[data-state="Dostępny"]{color:#00605b}.state[data-state="Serwis"]{color:#9a2d25}.empty-row{text-align:center;padding:38px}.motel-pages{display:flex;flex-wrap:wrap;gap:6px;padding-top:15px}.motel-pages button{min-height:44px;min-width:44px;border:2px solid #173c3b;border-radius:0;background:#efce98;color:#173c3b;font-weight:900;cursor:pointer}.motel-pages button[aria-current="page"]{background:#173c3b;color:#fff5de}.motel-pages button:disabled{opacity:.38;cursor:not-allowed}button:focus-visible,input:focus-visible{outline:4px solid #00605b;outline-offset:2px}@media(max-width:820px){.motel-board{padding:15px;border-width:7px}.desk{grid-template-columns:1fr}.desk fieldset{min-height:0}.motel-board h1{font-size:42px}.star{display:none}.motel-pages button:first-child,.motel-pages button:last-child{flex:1}}@media(max-width:420px){.motel-board>header{align-items:flex-start}.shift{display:none}.desk fieldset label{display:flex;margin:0}.motel-summary{display:block}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}
JavaScript
$(() => {
  const records = [{"room":"101","wing":"Patio","key":"A-11","guest":"—","status":"Dostępny","return":"Recepcja"},{"room":"102","wing":"Patio","key":"A-12","guest":"L. Nowak","status":"Wydany","return":"14:00"},{"room":"103","wing":"Patio","key":"A-13","guest":"—","status":"Serwis","return":"16:30"},{"room":"104","wing":"Patio","key":"A-14","guest":"—","status":"Dostępny","return":"Recepcja"},{"room":"201","wing":"Basen","key":"B-21","guest":"A. Lis","status":"Wydany","return":"12:00"},{"room":"202","wing":"Basen","key":"B-22","guest":"—","status":"Dostępny","return":"Recepcja"},{"room":"203","wing":"Basen","key":"B-23","guest":"M. Wrona","status":"Wydany","return":"11:30"},{"room":"204","wing":"Basen","key":"B-24","guest":"—","status":"Dostępny","return":"Recepcja"},{"room":"301","wing":"Neon","key":"C-31","guest":"—","status":"Serwis","return":"18:00"},{"room":"302","wing":"Neon","key":"C-32","guest":"I. Maj","status":"Wydany","return":"15:00"},{"room":"303","wing":"Neon","key":"C-33","guest":"—","status":"Dostępny","return":"Recepcja"},{"room":"304","wing":"Neon","key":"C-34","guest":"—","status":"Dostępny","return":"Recepcja"},{"room":"401","wing":"Taras","key":"D-41","guest":"K. Białek","status":"Wydany","return":"10:30"},{"room":"402","wing":"Taras","key":"D-42","guest":"—","status":"Dostępny","return":"Recepcja"}];
  const numericColumns = new Set(["room"]);
  const collator = new Intl.Collator('pl', { numeric: true, sensitivity: 'base' });
  const state = { query: '', sort: 'room', direction: 'asc', page: 1 };
  const pageSize = 4;
  const $root = $('#motel-table');
  const $body = $root.find('tbody');
  const $pages = $root.find('[data-pages]');
  const $status = $root.find('[role="status"]');

  function filteredRecords() {
    const query = state.query.trim().toLocaleLowerCase('pl');
    return records.filter((record) => {
      const matchesQuery = !query || Object.values(record).join(' ').toLocaleLowerCase('pl').includes(query);
      const status = $root.find('input[name="motel-status"]:checked').val();
      const matchesFilters = !status || record.status === status;
      return matchesQuery && matchesFilters;
    });
  }

  function compare(left, right) {
    const key = state.sort;
    const result = numericColumns.has(key)
      ? Number(left[key]) - Number(right[key])
      : collator.compare(String(left[key]), String(right[key]));
    return state.direction === 'asc' ? result : -result;
  }

  function rowTemplate(record) {
    return `<tr><th scope="row">${record.room}</th><td>${record.wing}</td><td><span class="key-tag">${record.key}</span></td><td>${record.guest}</td><td><span class="state" data-state="${record.status}">${record.status}</span></td><td>${record.return}</td></tr>`;
  }

  function render() {
    const result = filteredRecords().sort(compare);
    const totalPages = Math.max(1, Math.ceil(result.length / pageSize));
    state.page = Math.min(state.page, totalPages);
    const visible = result.slice((state.page - 1) * pageSize, state.page * pageSize);

    $body.html(visible.length
      ? visible.map(rowTemplate).join('')
      : '<tr><td class="empty-row" colspan="6">Brak wyników. Zmień filtry lub wyczyść wyszukiwanie.</td></tr>');

    $root.find('thead th').removeAttr('aria-sort');
    $root.find('[data-sort]').each(function () {
      const active = this.dataset.sort === state.sort;
      $(this).find('[data-sort-mark]').text(active ? (state.direction === 'asc' ? '↑' : '↓') : '↕');
      if (active) $(this).closest('th').attr('aria-sort', state.direction === 'asc' ? 'ascending' : 'descending');
    });

    const buttons = [];
    buttons.push(`<button type="button" data-page="${state.page - 1}" ${state.page === 1 ? 'disabled' : ''}>Poprzednia</button>`);
    for (let page = 1; page <= totalPages; page += 1) {
      buttons.push(`<button type="button" data-page="${page}" ${page === state.page ? 'aria-current="page"' : ''} aria-label="Strona ${page}">${page}</button>`);
    }
    buttons.push(`<button type="button" data-page="${state.page + 1}" ${state.page === totalPages ? 'disabled' : ''}>Następna</button>`);
    $pages.html(buttons.join(''));

    const direction = state.direction === 'asc' ? 'rosnąco' : 'malejąco';
    $status.text(`Wyniki: ${result.length} · Strona ${state.page} z ${totalPages} · Sortowanie: ${direction}.`);
  }

  $root.on('input', '[data-search]', function () {
    state.query = this.value;
    state.page = 1;
    render();
  });
  $root.on('change', '[data-filter]', function () {
    state.page = 1;
    render();
  });
  $root.on('click', '[data-sort]', function () {
    const key = this.dataset.sort;
    state.direction = state.sort === key && state.direction === 'asc' ? 'desc' : 'asc';
    state.sort = key;
    state.page = 1;
    render();
    this.focus();
  });
  $root.on('click', '[data-page]', function () {
    if (this.disabled) return;
    state.page = Number(this.dataset.page);
    render();
    $pages.find(`[data-page="${state.page}"]`).focus();
  });
  $root.on('click', '[data-reset]', () => {
    $root.find('[data-search]').val('');
    $root.find('select[data-filter]').prop('selectedIndex', 0);
    $root.find('input[type="radio"][data-filter]').first().prop('checked', true);
    state.query = '';
    state.sort = 'room';
    state.direction = 'asc';
    state.page = 1;
    render();
    $root.find('[data-search]').focus();
  });

  render();
});
Biblioteki

JavaScript

Edytuj swój pen

Podaj adres e-mail użyty przy zapisie. Wyślemy link do edycji.

Link jest ważny przez 30 minut.