Skip to content

Commit 19201ee

Browse files
committed
perf: progressively render large station lists
1 parent dce3f1a commit 19201ee

1 file changed

Lines changed: 37 additions & 8 deletions

File tree

‎app.js‎

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
const API = 'https://raw.githubusercontent.com/AirwaveDB/api/main/v1';
2+
const PAGE_SIZE = 100;
23

34
const country = document.querySelector('#country');
45
const search = document.querySelector('#search');
56
const type = document.querySelector('#type');
67
const status = document.querySelector('#status');
78
const stationsEl = document.querySelector('#stations');
89
const summary = document.querySelector('#summary');
10+
const more = document.querySelector('#more');
911
const template = document.querySelector('#station-template');
1012

1113
let current = [];
14+
let visibleCount = PAGE_SIZE;
1215
let countryNames;
1316

1417
try {
@@ -41,19 +44,24 @@ function searchable(station) {
4144
].filter(Boolean).join(' ').toLocaleLowerCase();
4245
}
4346

44-
function render() {
47+
function filteredStations() {
4548
const query = search.value.trim().toLocaleLowerCase();
4649
const wantedType = type.value;
47-
const filtered = current.filter((station) => {
50+
return current.filter((station) => {
4851
if (wantedType && station.type !== wantedType) return false;
4952
if (query && !searchable(station).includes(query)) return false;
5053
return true;
5154
});
55+
}
56+
57+
function render() {
58+
const filtered = filteredStations();
59+
const visible = filtered.slice(0, visibleCount);
5260

5361
stationsEl.replaceChildren();
5462
const fragment = document.createDocumentFragment();
5563

56-
for (const station of filtered) {
64+
for (const station of visible) {
5765
const node = template.content.firstElementChild.cloneNode(true);
5866
node.querySelector('.station-name').textContent = station.name;
5967

@@ -68,7 +76,8 @@ function render() {
6876
node.querySelector('.station-meta').textContent = meta.join(' · ');
6977

7078
const tags = node.querySelector('.station-tags');
71-
for (const value of [...(station.categories || []), ...(station.tags || []).slice(0, 5)]) {
79+
const tagValues = [...new Set([...(station.categories || []), ...(station.tags || [])])].slice(0, 8);
80+
for (const value of tagValues) {
7281
const span = document.createElement('span');
7382
span.textContent = value;
7483
tags.append(span);
@@ -95,6 +104,7 @@ function render() {
95104
option.textContent = fmtStream(stream);
96105
select.append(option);
97106
});
107+
98108
play.addEventListener('click', async () => {
99109
const stream = streams[Number(select.value) || 0];
100110
if (!stream) return;
@@ -107,13 +117,26 @@ function render() {
107117
}
108118
});
109119
player.addEventListener('pause', () => { if (!player.ended) play.textContent = 'Play'; });
120+
player.addEventListener('error', () => { play.textContent = 'Failed'; });
110121
}
111122

112123
fragment.append(node);
113124
}
114125

115126
stationsEl.append(fragment);
116-
status.textContent = `${filtered.length.toLocaleString()} station${filtered.length === 1 ? '' : 's'}`;
127+
128+
const shown = visible.length;
129+
status.textContent = filtered.length
130+
? `Showing ${shown.toLocaleString()} of ${filtered.length.toLocaleString()} station${filtered.length === 1 ? '' : 's'}`
131+
: 'No stations match these filters';
132+
133+
more.hidden = shown >= filtered.length;
134+
more.textContent = `Show ${Math.min(PAGE_SIZE, filtered.length - shown).toLocaleString()} more`;
135+
}
136+
137+
function resetAndRender() {
138+
visibleCount = PAGE_SIZE;
139+
render();
117140
}
118141

119142
async function loadCountry(code) {
@@ -123,13 +146,15 @@ async function loadCountry(code) {
123146
status.textContent = '';
124147
search.disabled = true;
125148
type.disabled = true;
149+
more.hidden = true;
126150
return;
127151
}
128152

129153
status.textContent = 'Loading…';
130154
stationsEl.replaceChildren();
131155
search.disabled = true;
132156
type.disabled = true;
157+
more.hidden = true;
133158

134159
try {
135160
const response = await fetch(`${API}/countries/${code.toLowerCase()}.json`);
@@ -138,7 +163,7 @@ async function loadCountry(code) {
138163
current = data.stations || [];
139164
search.disabled = false;
140165
type.disabled = false;
141-
render();
166+
resetAndRender();
142167
} catch (error) {
143168
current = [];
144169
status.textContent = `Failed to load ${code}: ${error.message}`;
@@ -170,7 +195,11 @@ async function init() {
170195
}
171196

172197
country.addEventListener('change', () => loadCountry(country.value));
173-
search.addEventListener('input', render);
174-
type.addEventListener('change', render);
198+
search.addEventListener('input', resetAndRender);
199+
type.addEventListener('change', resetAndRender);
200+
more.addEventListener('click', () => {
201+
visibleCount += PAGE_SIZE;
202+
render();
203+
});
175204

176205
init();

0 commit comments

Comments
 (0)