tangled
alpha
login
or
join now
dunkirk.sh
/
hop
6
fork
atom
blazing fast link redirects on cloudflare kv
hop.dunkirk.sh/u/tacy
6
fork
atom
overview
issues
pulls
pipelines
bug: fix the virtual scrolling
dunkirk.sh
3 months ago
bcdc009b
3373c34d
verified
This commit was signed with the committer's
known signature
.
dunkirk.sh
SSH Key Fingerprint:
SHA256:DqcG0RXYExE26KiWo3VxJnsxswN1QNfTBvB+bdSpk80=
+79
-103
2 changed files
expand all
collapse all
unified
split
src
index.html
wrangler.toml
+75
-103
src/index.html
···
299
299
position: relative;
300
300
}
301
301
302
302
-
table {
303
303
-
width: 100%;
304
304
-
border-collapse: collapse;
305
305
-
background: var(--input-bg);
306
306
-
border: 0.0625rem solid var(--border-color);
307
307
-
border-radius: 0.25rem;
308
308
-
overflow: hidden;
309
309
-
}
310
310
-
311
311
-
th,
312
312
-
td {
313
313
-
padding: 0.75rem 0.875rem;
314
314
-
text-align: left;
315
315
-
border-bottom: 0.0625rem solid var(--border-color);
316
316
-
font-size: 0.8125rem;
317
317
-
}
318
318
-
319
319
-
th {
320
320
-
background: #252620;
321
321
-
color: var(--accent-primary);
322
322
-
font-weight: 700;
323
323
-
text-transform: uppercase;
324
324
-
font-size: 0.6875rem;
325
325
-
letter-spacing: 0.03125rem;
326
326
-
}
327
327
-
328
328
-
tr:last-child td {
329
329
-
border-bottom: none;
330
330
-
}
331
331
-
332
332
-
tr:hover {
333
333
-
background: #2d2e28;
334
334
-
}
335
335
-
336
302
.url-cell {
337
337
-
max-width: 25rem;
303
303
+
max-width: 100%;
338
304
overflow: hidden;
339
305
text-overflow: ellipsis;
340
306
white-space: nowrap;
341
307
color: var(--text-primary);
342
342
-
}
343
343
-
344
344
-
.short-cell {
345
345
-
color: var(--accent-bright);
346
346
-
font-weight: 700;
347
347
-
}
348
348
-
349
349
-
.short-cell a {
350
350
-
color: var(--accent-bright);
351
351
-
text-decoration: none;
352
352
-
}
353
353
-
354
354
-
.short-cell a:hover {
355
355
-
text-decoration: underline;
356
356
-
}
357
357
-
358
358
-
.date-cell {
359
359
-
color: var(--text-muted);
360
360
-
font-size: 0.75rem;
361
361
-
}
362
362
-
363
363
-
.actions-cell {
364
364
-
text-align: right;
365
308
}
366
309
367
310
.btn-small {
···
657
600
const totalHeight = urls.length * ROW_HEIGHT;
658
601
urlsTable.innerHTML = '';
659
602
660
660
-
const viewport = document.createElement('div');
661
661
-
viewport.style.height = `${totalHeight}px`;
662
662
-
viewport.style.position = 'relative';
603
603
+
// Create container with sticky header
604
604
+
const container = document.createElement('div');
605
605
+
container.style.position = 'relative';
606
606
+
container.style.height = '100%';
663
607
664
664
-
const table = document.createElement('table');
665
665
-
table.style.width = '100%';
666
666
-
table.style.borderCollapse = 'collapse';
667
667
-
table.style.tableLayout = 'fixed';
668
668
-
669
669
-
const thead = document.createElement('thead');
670
670
-
thead.style.position = 'sticky';
671
671
-
thead.style.top = '0';
672
672
-
thead.style.background = 'var(--input-bg)';
673
673
-
thead.style.zIndex = '10';
674
674
-
thead.innerHTML = `
675
675
-
<tr>
676
676
-
<th style="padding: 0.75rem 0.875rem; text-align: left; border-bottom: 0.0625rem solid var(--border-color); font-size: 0.6875rem; color: var(--accent-primary); font-weight: 700; text-transform: uppercase; letter-spacing: 0.03125rem; width: 15%;">short</th>
677
677
-
<th style="padding: 0.75rem 0.875rem; text-align: left; border-bottom: 0.0625rem solid var(--border-color); font-size: 0.6875rem; color: var(--accent-primary); font-weight: 700; text-transform: uppercase; letter-spacing: 0.03125rem; width: 50%;">url</th>
678
678
-
<th style="padding: 0.75rem 0.875rem; text-align: left; border-bottom: 0.0625rem solid var(--border-color); font-size: 0.6875rem; color: var(--accent-primary); font-weight: 700; text-transform: uppercase; letter-spacing: 0.03125rem; width: 15%;">created</th>
679
679
-
<th style="padding: 0.75rem 0.875rem; text-align: right; border-bottom: 0.0625rem solid var(--border-color); font-size: 0.6875rem; color: var(--accent-primary); font-weight: 700; text-transform: uppercase; letter-spacing: 0.03125rem; width: 20%;">actions</th>
680
680
-
</tr>
608
608
+
// Create sticky header
609
609
+
const header = document.createElement('div');
610
610
+
header.style.display = 'grid';
611
611
+
header.style.gridTemplateColumns = '15% 50% 15% 20%';
612
612
+
header.style.position = 'sticky';
613
613
+
header.style.top = '0';
614
614
+
header.style.background = 'var(--input-bg)';
615
615
+
header.style.zIndex = '10';
616
616
+
header.style.borderBottom = '0.0625rem solid var(--border-color)';
617
617
+
header.innerHTML = `
618
618
+
<div style="padding: 0.75rem 0.875rem; font-size: 0.6875rem; color: var(--accent-primary); font-weight: 700; text-transform: uppercase; letter-spacing: 0.03125rem;">short</div>
619
619
+
<div style="padding: 0.75rem 0.875rem; font-size: 0.6875rem; color: var(--accent-primary); font-weight: 700; text-transform: uppercase; letter-spacing: 0.03125rem;">url</div>
620
620
+
<div style="padding: 0.75rem 0.875rem; font-size: 0.6875rem; color: var(--accent-primary); font-weight: 700; text-transform: uppercase; letter-spacing: 0.03125rem;">created</div>
621
621
+
<div style="padding: 0.75rem 0.875rem; text-align: right; font-size: 0.6875rem; color: var(--accent-primary); font-weight: 700; text-transform: uppercase; letter-spacing: 0.03125rem;">actions</div>
681
622
`;
682
623
683
683
-
const tbody = document.createElement('tbody');
684
684
-
table.appendChild(thead);
685
685
-
table.appendChild(tbody);
686
686
-
viewport.appendChild(table);
687
687
-
urlsTable.appendChild(viewport);
624
624
+
// Create scrollable content area
625
625
+
const scrollContent = document.createElement('div');
626
626
+
scrollContent.style.position = 'relative';
627
627
+
scrollContent.style.height = `${totalHeight}px`;
628
628
+
629
629
+
container.appendChild(header);
630
630
+
container.appendChild(scrollContent);
631
631
+
urlsTable.appendChild(container);
632
632
+
633
633
+
let lastScrollTop = -1;
634
634
+
let isInitialRender = true;
688
635
689
636
function updateVisibleRows() {
690
637
const scrollTop = urlsTable.scrollTop;
···
694
641
const startIndex = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - buffer);
695
642
const endIndex = Math.min(urls.length, Math.ceil((scrollTop + viewportHeight) / ROW_HEIGHT) + buffer);
696
643
697
697
-
tbody.innerHTML = '';
644
644
+
// Only update if scroll position changed significantly (skip this check on initial render)
645
645
+
if (!isInitialRender && Math.abs(scrollTop - lastScrollTop) < ROW_HEIGHT / 2) {
646
646
+
return;
647
647
+
}
648
648
+
isInitialRender = false;
649
649
+
lastScrollTop = scrollTop;
650
650
+
651
651
+
// Clear existing rows
652
652
+
scrollContent.innerHTML = '';
698
653
699
654
for (let i = startIndex; i < endIndex; i++) {
700
655
const item = urls[i];
701
701
-
const row = document.createElement('tr');
702
702
-
row.style.height = `${ROW_HEIGHT}px`;
656
656
+
657
657
+
const row = document.createElement('div');
658
658
+
row.style.display = 'grid';
659
659
+
row.style.gridTemplateColumns = '15% 50% 15% 20%';
703
660
row.style.position = 'absolute';
704
704
-
row.style.top = `${i * ROW_HEIGHT + 41}px`;
661
661
+
row.style.top = `${i * ROW_HEIGHT}px`;
705
662
row.style.left = '0';
706
663
row.style.right = '0';
707
707
-
row.style.display = 'table';
708
708
-
row.style.width = '100%';
709
709
-
row.style.tableLayout = 'fixed';
664
664
+
row.style.height = `${ROW_HEIGHT}px`;
665
665
+
row.style.borderBottom = '0.0625rem solid var(--border-color)';
666
666
+
row.style.transition = 'background 0.15s';
710
667
row.dataset.short = item.shortCode;
668
668
+
669
669
+
row.addEventListener('mouseenter', () => {
670
670
+
row.style.background = '#2d2e28';
671
671
+
});
672
672
+
row.addEventListener('mouseleave', () => {
673
673
+
row.style.background = '';
674
674
+
});
711
675
712
676
row.innerHTML = `
713
713
-
<td style="padding: 0.75rem 0.875rem; border-bottom: 0.0625rem solid var(--border-color); font-size: 0.8125rem; color: var(--accent-bright); font-weight: 700; width: 15%;">
677
677
+
<div style="padding: 0.75rem 0.875rem; font-size: 0.8125rem; color: var(--accent-bright); font-weight: 700; display: flex; align-items: center;">
714
678
<a href="/${item.shortCode}" target="_blank" style="color: var(--accent-bright); text-decoration: none;">/${item.shortCode}</a>
715
715
-
</td>
716
716
-
<td style="padding: 0.75rem 0.875rem; border-bottom: 0.0625rem solid var(--border-color); font-size: 0.8125rem; max-width: 25rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-primary); width: 50%;" title="${item.url}">${item.url}</td>
717
717
-
<td style="padding: 0.75rem 0.875rem; border-bottom: 0.0625rem solid var(--border-color); font-size: 0.75rem; color: var(--text-muted); width: 15%;">${new Date(item.created).toLocaleDateString()}</td>
718
718
-
<td style="padding: 0.75rem 0.875rem; border-bottom: 0.0625rem solid var(--border-color); text-align: right; width: 20%;">
719
719
-
<button onclick="editUrl('${item.shortCode}', '${item.url.replace(/'/g, "\\'")}')" class="btn-small">edit</button>
720
720
-
<button onclick="deleteUrl('${item.shortCode}')" class="btn-small btn-delete">delete</button>
721
721
-
</td>
679
679
+
</div>
680
680
+
<div class="url-cell" style="padding: 0.75rem 0.875rem; font-size: 0.8125rem; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: flex; align-items: center;" title="${item.url}">${item.url}</div>
681
681
+
<div style="padding: 0.75rem 0.875rem; font-size: 0.75rem; color: var(--text-muted); display: flex; align-items: center;">${new Date(item.created).toLocaleDateString()}</div>
682
682
+
<div style="padding: 0.75rem 0.875rem; text-align: right; display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem;">
683
683
+
<button onclick="editUrl('${item.shortCode}', '${item.url.replace(/'/g, "\\'")}')" class="btn-small">✏️</button>
684
684
+
<button onclick="deleteUrl('${item.shortCode}')" class="btn-small btn-delete">🗑️</button>
685
685
+
</div>
722
686
`;
723
687
724
724
-
tbody.appendChild(row);
688
688
+
scrollContent.appendChild(row);
725
689
}
726
690
}
727
691
728
728
-
urlsTable.addEventListener('scroll', updateVisibleRows);
729
729
-
updateVisibleRows();
692
692
+
urlsTable.addEventListener('scroll', updateVisibleRows, { passive: true });
693
693
+
694
694
+
// Use requestAnimationFrame to ensure DOM is ready before initial render
695
695
+
requestAnimationFrame(() => {
696
696
+
updateVisibleRows();
697
697
+
});
730
698
}
731
699
732
700
function updateUrlCount() {
···
823
791
};
824
792
825
793
window.editUrl = (shortCode, currentUrl) => {
826
826
-
const row = document.querySelector(`tr[data-short="${shortCode}"]`);
794
794
+
const row = document.querySelector(`div[data-short="${shortCode}"]`);
795
795
+
if (!row) return;
796
796
+
827
797
const urlCell = row.querySelector(".url-cell");
828
798
829
799
urlCell.innerHTML = `
···
871
841
};
872
842
873
843
window.cancelEdit = (shortCode, originalUrl) => {
874
874
-
const row = document.querySelector(`tr[data-short="${shortCode}"]`);
844
844
+
const row = document.querySelector(`div[data-short="${shortCode}"]`);
845
845
+
if (!row) return;
846
846
+
875
847
const urlCell = row.querySelector(".url-cell");
876
848
urlCell.innerHTML = originalUrl;
877
849
urlCell.title = originalUrl;
+4
wrangler.toml
···
8
8
[observability]
9
9
enabled = true
10
10
11
11
+
[[routes]]
12
12
+
pattern = "hop.dunkirk.sh"
13
13
+
custom_domain = true
14
14
+
11
15
[[kv_namespaces]]
12
16
binding = "HOP"
13
17
id = "ae7cd39a622b466d876b8410d22d1397"