blazing fast link redirects on cloudflare kv hop.dunkirk.sh/u/tacy

feat: add triple click delete and fix edit mode

dunkirk.sh 2c9d3de0 291614b6

verified
+69 -25
+69 -25
src/index.html
··· 350 350 display: flex; 351 351 gap: 0.5rem; 352 352 align-items: center; 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 + 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 - window.fetch = function(...args) { 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 + 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 - await fetch('/api/logout', { method: 'POST' }); 512 - } catch (e) {} 514 + await fetch('/api/logout', {method: 'POST'}); 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 - 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 - 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 - 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 - 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 - 672 + 670 673 row.addEventListener('mouseenter', () => { 671 674 row.style.background = '#2d2e28'; 672 675 }); ··· 690 693 } 691 694 } 692 695 693 - urlsTable.addEventListener('scroll', updateVisibleRows, { passive: true }); 694 - 696 + urlsTable.addEventListener('scroll', updateVisibleRows, {passive: true}); 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 - 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 - 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 - if (!confirm(`Delete /${shortCode}?`)) return; 857 + const now = Date.now(); 858 + const btn = event.target; 855 859 856 - try { 857 - const response = await fetch(`/api/urls/${shortCode}`, { 858 - method: "DELETE", 859 - }); 860 + // Initialize tracker if doesn't exist 861 + if (!deleteClickTracker[shortCode]) { 862 + deleteClickTracker[shortCode] = {count: 0, time: 0}; 863 + } 860 864 861 - if (!response.ok) { 862 - const data = await response.json(); 863 - throw new Error(data.error || "Failed to delete URL"); 864 - } 865 + const tracker = deleteClickTracker[shortCode]; 865 866 866 - allUrls = allUrls.filter((u) => u.shortCode !== shortCode); 867 - applySearch(); 868 - } catch (error) { 869 - alert(error.message); 867 + // Reset if too much time passed 868 + if (now - tracker.time > 500) { 869 + tracker.count = 0; 870 + btn.textContent = '🗑️'; 871 + } 872 + 873 + tracker.count++; 874 + tracker.time = now; 875 + 876 + if (tracker.count === 1) { 877 + // First click 878 + btn.textContent = '❓'; 879 + setTimeout(() => { 880 + if (deleteClickTracker[shortCode]?.count === 1) { 881 + delete deleteClickTracker[shortCode]; 882 + btn.textContent = '🗑️'; 883 + } 884 + }, 500); 885 + } else if (tracker.count === 2) { 886 + // Second click 887 + btn.textContent = '⁉️'; 888 + setTimeout(() => { 889 + if (deleteClickTracker[shortCode]?.count === 2) { 890 + delete deleteClickTracker[shortCode]; 891 + btn.textContent = '🗑️'; 892 + } 893 + }, 500); 894 + } else if (tracker.count >= 3) { 895 + // Third click - actually delete 896 + delete deleteClickTracker[shortCode]; 897 + 898 + try { 899 + const response = await fetch(`/api/urls/${shortCode}`, { 900 + method: "DELETE", 901 + }); 902 + 903 + if (!response.ok) { 904 + const data = await response.json(); 905 + throw new Error(data.error || "Failed to delete URL"); 906 + } 907 + 908 + allUrls = allUrls.filter((u) => u.shortCode !== shortCode); 909 + applySearch(); 910 + } catch (error) { 911 + alert(error.message); 912 + btn.textContent = '🗑️'; 913 + } 870 914 } 871 915 }; 872 916