Katalog znaczków — karta kolekcji
NarzędziaPodgląd
Ładowanie podglądu…
Albumowy katalog z filtrami kraju i dekady, sortowaniem zbioru oraz numerowanymi stronami.
Kod źródłowy
Licencja MIT — także do użytku komercyjnego. Zachowaj informacje o autorze i licencji.
HTML
<main class="stamp-album" id="stamp-table"><header><div><p>ALBUM 04 / EUROPA I ŚWIAT</p><h1>Katalog znaczków</h1></div><div class="postmark" aria-hidden="true"><b>13</b><span>X</span><b>26</b></div></header>
<section class="album-tools" aria-label="Filtry katalogu"><label>Motyw lub numer katalogowy<input type="search" data-search placeholder="np. ogrody"></label><label>Kraj<select id="stamp-country" data-filter><option value="">Wszystkie kraje</option><option>Polska</option><option>Japonia</option><option>Finlandia</option><option>Meksyk</option><option>Portugalia</option><option>Kanada</option><option>Nowa Zelandia</option></select></label><label>Dekada<select id="stamp-decade" data-filter><option value="">Każda dekada</option><option value="1960">Lata 60.</option><option value="1970">Lata 70.</option><option value="1980">Lata 80.</option><option value="1990">Lata 90.</option></select></label><button type="button" data-reset>Usuń filtry</button></section>
<div class="album-meta"><p role="status" aria-live="polite"></p><span>** czysty ze śladem · * kasowany</span></div><div class="table-scroll" tabindex="0" aria-label="Tabela kolekcji znaczków — przewijaj poziomo na małym ekranie"><table><caption>Karta kolekcji numer 17</caption><thead><tr>
<th scope="col"><button type="button" data-sort="catalog">Nr katalogu <span data-sort-mark aria-hidden="true">↕</span></button></th><th scope="col"><button type="button" data-sort="country">Kraj <span data-sort-mark aria-hidden="true">↕</span></button></th><th scope="col"><button type="button" data-sort="year">Rok <span data-sort-mark aria-hidden="true">↕</span></button></th><th scope="col"><button type="button" data-sort="theme">Motyw <span data-sort-mark aria-hidden="true">↕</span></button></th><th scope="col"><button type="button" data-sort="print">Druk <span data-sort-mark aria-hidden="true">↕</span></button></th><th scope="col"><button type="button" data-sort="value">Wycena <span data-sort-mark aria-hidden="true">↕</span></button></th>
</tr></thead><tbody><tr><td colspan="6">Wyjmowanie karty z albumu…</td></tr></tbody></table></div><nav class="album-pages" data-pages aria-label="Stronicowanie tabeli"></nav>
</main>
CSS
*{box-sizing:border-box}body{margin:0;background:#182943;color:#17243b;font-family:Verdana,Arial,sans-serif}.stamp-album{min-height:100vh;max-width:1160px;margin:auto;padding:clamp(18px,4vw,48px);background:#ede8dc;border-left:10px solid #bd3e38;border-right:10px solid #bd3e38}.stamp-album>header{display:flex;justify-content:space-between;gap:20px;align-items:center;border-bottom:2px solid #17243b;padding-bottom:16px}.stamp-album header p{margin:0 0 8px;font-size:10px;font-weight:800;letter-spacing:.18em}.stamp-album h1{margin:0;font:700 clamp(36px,6vw,70px)/.9 Georgia,serif;letter-spacing:-.04em}.postmark{width:88px;height:88px;border:3px double #bd3e38;border-radius:50%;display:flex;align-items:center;justify-content:center;gap:5px;color:#bd3e38;transform:rotate(-9deg)}.postmark span{font-weight:900}.album-tools{display:grid;grid-template-columns:1.3fr .7fr .7fr auto;gap:10px;align-items:end;margin-top:19px;padding:14px;border:1px dashed #17243b;background:#f8f5ec}.album-tools label{display:grid;gap:6px;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.05em}.album-tools input,.album-tools select,.album-tools button{height:46px;border:1px solid #17243b;border-radius:0;background:#fff;color:#17243b;font:inherit;padding:0 10px}.album-tools button{background:#bd3e38;color:#fff;border-color:#bd3e38;font-weight:800;cursor:pointer}.album-meta{display:flex;justify-content:space-between;gap:18px;font-size:11px;padding:11px 0 8px}.album-meta p{margin:0}.table-scroll{max-width:100%;overflow-x:auto;border:1px solid #17243b;background:#fffdf7}.table-scroll:focus-visible{outline:4px solid #bd3e38;outline-offset:3px}table{width:100%;min-width:760px;border-collapse:collapse}caption{text-align:left;padding:13px;background:#17243b;color:#fff;font:700 17px Georgia,serif}th,td{text-align:left;padding:12px;border-bottom:1px dashed #9b9486;border-right:1px dotted #c5bdad}thead{background:#dbe1e8}thead th{padding:0 9px}th button{width:100%;min-height:48px;border:0;background:transparent;color:#17243b;font-size:11px;font-weight:800;text-align:left;text-transform:uppercase;cursor:pointer}th[aria-sort]{box-shadow:inset 0 -5px #bd3e38}.catalog-code{font:700 13px "Courier New",monospace;color:#bd3e38}.stamp-theme{font-family:Georgia,serif;font-size:17px}.stamp-theme small,td small,tbody th small{display:block;margin-top:3px;color:#665f54;font:10px Verdana,sans-serif;text-transform:uppercase;letter-spacing:.06em}.value{font-weight:800;white-space:nowrap}.empty-row{text-align:center;padding:40px}.album-pages{display:flex;flex-wrap:wrap;align-items:center;gap:7px;padding-top:16px}.album-pages button{min-width:44px;min-height:44px;border:1px solid #17243b;border-radius:0;background:#fffdf7;color:#17243b;font-weight:800;cursor:pointer}.album-pages button[aria-current="page"]{background:#17243b;color:#fff}.album-pages button:disabled{opacity:.4;cursor:not-allowed}button:focus-visible,input:focus-visible,select:focus-visible{outline:4px solid #bd3e38;outline-offset:2px}@media(max-width:720px){.stamp-album{padding:15px 12px;border-width:5px}.postmark{width:62px;height:62px}.album-tools{grid-template-columns:1fr 1fr}.album-tools label:first-child{grid-column:1/-1}.album-meta{flex-direction:column;gap:4px}.album-pages button:first-child,.album-pages button:last-child{flex:1}}@media(max-width:430px){.stamp-album h1{font-size:38px}.postmark{display:none}.album-tools{grid-template-columns:1fr}.album-tools label:first-child{grid-column:auto}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}
JavaScript
$(() => {
const records = [{"catalog":"PL-0672","country":"Polska","year":1960,"decade":"1960","theme":"Polska szkoła plakatu","print":"offset","condition":"**","value":28},{"catalog":"PL-1128","country":"Polska","year":1973,"decade":"1970","theme":"Latarnie morskie","print":"staloryt","condition":"*","value":34},{"catalog":"PL-1480","country":"Polska","year":1980,"decade":"1980","theme":"Flora Tatr","print":"rotograwiura","condition":"**","value":19},{"catalog":"PL-1904","country":"Polska","year":1991,"decade":"1990","theme":"Historia lotnictwa","print":"offset","condition":"**","value":22},{"catalog":"JP-0841","country":"Japonia","year":1964,"decade":"1960","theme":"Igrzyska w Tokio","print":"wklęsłodruk","condition":"*","value":76},{"catalog":"JP-1210","country":"Japonia","year":1978,"decade":"1970","theme":"Ogrody Kioto","print":"wklęsłodruk","condition":"**","value":44},{"catalog":"FI-0522","country":"Finlandia","year":1967,"decade":"1960","theme":"Architektura Aalto","print":"staloryt","condition":"**","value":58},{"catalog":"FI-0908","country":"Finlandia","year":1985,"decade":"1980","theme":"Ptaki północy","print":"offset","condition":"*","value":31},{"catalog":"MX-0764","country":"Meksyk","year":1968,"decade":"1960","theme":"Sztuka ludowa","print":"rotograwiura","condition":"**","value":47},{"catalog":"MX-1331","country":"Meksyk","year":1992,"decade":"1990","theme":"Maski rytualne","print":"offset","condition":"**","value":24},{"catalog":"PT-0930","country":"Portugalia","year":1975,"decade":"1970","theme":"Azulejos","print":"offset","condition":"*","value":52},{"catalog":"PT-1455","country":"Portugalia","year":1989,"decade":"1980","theme":"Żaglowce","print":"staloryt","condition":"**","value":39},{"catalog":"CA-0612","country":"Kanada","year":1962,"decade":"1960","theme":"Arktyka","print":"staloryt","condition":"*","value":64},{"catalog":"CA-1050","country":"Kanada","year":1979,"decade":"1970","theme":"Parki narodowe","print":"offset","condition":"**","value":27},{"catalog":"NZ-0992","country":"Nowa Zelandia","year":1984,"decade":"1980","theme":"Paprocie","print":"wklęsłodruk","condition":"**","value":36}];
const numericColumns = new Set(["year", "value"]);
const collator = new Intl.Collator('pl', { numeric: true, sensitivity: 'base' });
const state = { query: '', sort: 'year', direction: 'asc', page: 1 };
const pageSize = 4;
const $root = $('#stamp-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 country = $('#stamp-country').val();
const decade = $('#stamp-decade').val();
const matchesFilters = (!country || record.country === country) && (!decade || record.decade === decade);
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 class="catalog-code" scope="row">${record.catalog}<small>${record.condition}</small></th><td>${record.country}</td><td>${record.year}</td><td class="stamp-theme">${record.theme}</td><td>${record.print}</td><td class="value">${record.value} zł</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 = 'year';
state.direction = 'asc';
state.page = 1;
render();
$root.find('[data-search]').focus();
});
render();
});
Edytuj swój pen
Podaj adres e-mail użyty przy zapisie. Wyślemy link do edycji.