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
feat: add triple click delete and fix edit mode
dunkirk.sh
3 months ago
2c9d3de0
291614b6
verified
This commit was signed with the committer's
known signature
.
dunkirk.sh
SSH Key Fingerprint:
SHA256:DqcG0RXYExE26KiWo3VxJnsxswN1QNfTBvB+bdSpk80=
+69
-25
1 changed file
expand all
collapse all
unified
split
src
index.html
+69
-25
src/index.html
···
350
350
display: flex;
351
351
gap: 0.5rem;
352
352
align-items: center;
353
353
+
width: 100%;
353
354
}
354
355
355
356
.edit-form input {
356
357
margin: 0;
357
358
padding: 0.5rem 0.625rem;
358
359
font-size: 0.75rem;
360
360
+
flex: 1;
359
361
}
360
362
361
363
.edit-form button {
···
426
428
427
429
// Add auth header to all API requests
428
430
const originalFetch = window.fetch;
429
429
-
window.fetch = function(...args) {
431
431
+
window.fetch = function (...args) {
430
432
const [url, config] = args;
431
433
if (typeof url === 'string' && url.startsWith('/api/')) {
432
434
const token = localStorage.getItem('hop_session');
···
477
479
let filteredUrls = [];
478
480
let debounceTimer;
479
481
let resultTimeout;
482
482
+
let deleteClickTracker = {};
480
483
const ROW_HEIGHT = 60;
481
484
482
485
document.addEventListener("keydown", (e) => {
···
508
511
509
512
async function logout() {
510
513
try {
511
511
-
await fetch('/api/logout', { method: 'POST' });
512
512
-
} catch (e) {}
514
514
+
await fetch('/api/logout', {method: 'POST'});
515
515
+
} catch (e) { }
513
516
localStorage.removeItem('hop_session');
514
517
window.location.href = '/login';
515
518
}
···
534
537
searchInput.addEventListener("input", () => {
535
538
// Instant search on loaded data
536
539
applySearch();
537
537
-
540
540
+
538
541
// Debounced server search for more results
539
542
clearTimeout(debounceTimer);
540
543
debounceTimer = setTimeout(() => {
···
562
565
try {
563
566
const response = await fetch(`/api/urls?limit=1000&search=${encodeURIComponent(query)}`);
564
567
const data = await response.json();
565
565
-
568
568
+
566
569
// Merge server results with existing data
567
570
const existingCodes = new Set(allUrls.map(u => u.shortCode));
568
571
const newUrls = data.urls.filter(u => !existingCodes.has(u.shortCode));
569
569
-
572
572
+
570
573
if (newUrls.length > 0) {
571
574
allUrls.push(...newUrls);
572
575
applySearch();
···
654
657
655
658
for (let i = startIndex; i < endIndex; i++) {
656
659
const item = urls[i];
657
657
-
660
660
+
658
661
const row = document.createElement('div');
659
662
row.style.display = 'grid';
660
663
row.style.gridTemplateColumns = '15% 50% 15% 20%';
···
666
669
row.style.borderBottom = '0.0625rem solid var(--border-color)';
667
670
row.style.transition = 'background 0.15s';
668
671
row.dataset.short = item.shortCode;
669
669
-
672
672
+
670
673
row.addEventListener('mouseenter', () => {
671
674
row.style.background = '#2d2e28';
672
675
});
···
690
693
}
691
694
}
692
695
693
693
-
urlsTable.addEventListener('scroll', updateVisibleRows, { passive: true });
694
694
-
696
696
+
urlsTable.addEventListener('scroll', updateVisibleRows, {passive: true});
697
697
+
695
698
// Use requestAnimationFrame to ensure DOM is ready before initial render
696
699
requestAnimationFrame(() => {
697
700
updateVisibleRows();
···
794
797
window.editUrl = (shortCode, currentUrl) => {
795
798
const row = document.querySelector(`div[data-short="${shortCode}"]`);
796
799
if (!row) return;
797
797
-
800
800
+
798
801
const urlCell = row.querySelector(".url-cell");
799
802
800
803
urlCell.innerHTML = `
···
844
847
window.cancelEdit = (shortCode, originalUrl) => {
845
848
const row = document.querySelector(`div[data-short="${shortCode}"]`);
846
849
if (!row) return;
847
847
-
850
850
+
848
851
const urlCell = row.querySelector(".url-cell");
849
852
urlCell.innerHTML = originalUrl;
850
853
urlCell.title = originalUrl;
851
854
};
852
855
853
856
window.deleteUrl = async (shortCode) => {
854
854
-
if (!confirm(`Delete /${shortCode}?`)) return;
857
857
+
const now = Date.now();
858
858
+
const btn = event.target;
855
859
856
856
-
try {
857
857
-
const response = await fetch(`/api/urls/${shortCode}`, {
858
858
-
method: "DELETE",
859
859
-
});
860
860
+
// Initialize tracker if doesn't exist
861
861
+
if (!deleteClickTracker[shortCode]) {
862
862
+
deleteClickTracker[shortCode] = {count: 0, time: 0};
863
863
+
}
860
864
861
861
-
if (!response.ok) {
862
862
-
const data = await response.json();
863
863
-
throw new Error(data.error || "Failed to delete URL");
864
864
-
}
865
865
+
const tracker = deleteClickTracker[shortCode];
865
866
866
866
-
allUrls = allUrls.filter((u) => u.shortCode !== shortCode);
867
867
-
applySearch();
868
868
-
} catch (error) {
869
869
-
alert(error.message);
867
867
+
// Reset if too much time passed
868
868
+
if (now - tracker.time > 500) {
869
869
+
tracker.count = 0;
870
870
+
btn.textContent = '🗑️';
871
871
+
}
872
872
+
873
873
+
tracker.count++;
874
874
+
tracker.time = now;
875
875
+
876
876
+
if (tracker.count === 1) {
877
877
+
// First click
878
878
+
btn.textContent = '❓';
879
879
+
setTimeout(() => {
880
880
+
if (deleteClickTracker[shortCode]?.count === 1) {
881
881
+
delete deleteClickTracker[shortCode];
882
882
+
btn.textContent = '🗑️';
883
883
+
}
884
884
+
}, 500);
885
885
+
} else if (tracker.count === 2) {
886
886
+
// Second click
887
887
+
btn.textContent = '⁉️';
888
888
+
setTimeout(() => {
889
889
+
if (deleteClickTracker[shortCode]?.count === 2) {
890
890
+
delete deleteClickTracker[shortCode];
891
891
+
btn.textContent = '🗑️';
892
892
+
}
893
893
+
}, 500);
894
894
+
} else if (tracker.count >= 3) {
895
895
+
// Third click - actually delete
896
896
+
delete deleteClickTracker[shortCode];
897
897
+
898
898
+
try {
899
899
+
const response = await fetch(`/api/urls/${shortCode}`, {
900
900
+
method: "DELETE",
901
901
+
});
902
902
+
903
903
+
if (!response.ok) {
904
904
+
const data = await response.json();
905
905
+
throw new Error(data.error || "Failed to delete URL");
906
906
+
}
907
907
+
908
908
+
allUrls = allUrls.filter((u) => u.shortCode !== shortCode);
909
909
+
applySearch();
910
910
+
} catch (error) {
911
911
+
alert(error.message);
912
912
+
btn.textContent = '🗑️';
913
913
+
}
870
914
}
871
915
};
872
916