The codebase that powers boop.cat boop.cat

fix: custom domains not working, add loading for checking for custom domains

+36 -7
+25 -3
client/src/pages/DashboardSite.jsx
··· 24 Eye, 25 EyeOff, 26 Trash2, 27 - Plus 28 } from 'lucide-react'; 29 30 function Toast({ message, onClose }) { ··· 357 const [visibleEnvKeys, setVisibleEnvKeys] = useState(new Set()); 358 const [envModalOpen, setEnvModalOpen] = useState(false); 359 const [editingEnv, setEditingEnv] = useState(null); // { key, value } for edit mode 360 361 const repoInfo = useMemo(() => { 362 const gitUrl = site?.git?.url || ''; ··· 707 } 708 709 async function pollCustomDomain(customDomainId) { 710 try { 711 const data = await api( 712 `/api/sites/${encodeURIComponent(site.id)}/custom-domains/${encodeURIComponent(customDomainId)}/poll`, 713 { method: 'POST' } 714 ); 715 setCustomDomains((prev) => prev.map((d) => (d.id === customDomainId ? { ...d, ...data } : d))); 716 } catch (e) { 717 console.error('Poll failed:', e); 718 } 719 } 720 ··· 1097 </div> 1098 <div className="actions"> 1099 {isPending && ( 1100 - <button className="btn ghost" onClick={() => pollCustomDomain(d.id)}> 1101 - Check 1102 </button> 1103 )} 1104 <button className="btn danger" onClick={() => removeCustomDomain(d.id)}>
··· 24 Eye, 25 EyeOff, 26 Trash2, 27 + Plus, 28 + Loader2 29 } from 'lucide-react'; 30 31 function Toast({ message, onClose }) { ··· 358 const [visibleEnvKeys, setVisibleEnvKeys] = useState(new Set()); 359 const [envModalOpen, setEnvModalOpen] = useState(false); 360 const [editingEnv, setEditingEnv] = useState(null); // { key, value } for edit mode 361 + const [pollingDomains, setPollingDomains] = useState(new Set()); 362 363 const repoInfo = useMemo(() => { 364 const gitUrl = site?.git?.url || ''; ··· 709 } 710 711 async function pollCustomDomain(customDomainId) { 712 + setPollingDomains((prev) => new Set(prev).add(customDomainId)); 713 try { 714 const data = await api( 715 `/api/sites/${encodeURIComponent(site.id)}/custom-domains/${encodeURIComponent(customDomainId)}/poll`, 716 { method: 'POST' } 717 ); 718 setCustomDomains((prev) => prev.map((d) => (d.id === customDomainId ? { ...d, ...data } : d))); 719 + setToast('Domain status updated.'); 720 } catch (e) { 721 console.error('Poll failed:', e); 722 + setError(e.message || 'Failed to check domain status.'); 723 + } finally { 724 + setPollingDomains((prev) => { 725 + const next = new Set(prev); 726 + next.delete(customDomainId); 727 + return next; 728 + }); 729 } 730 } 731 ··· 1108 </div> 1109 <div className="actions"> 1110 {isPending && ( 1111 + <button 1112 + className="btn ghost" 1113 + disabled={pollingDomains.has(d.id)} 1114 + onClick={() => pollCustomDomain(d.id)} 1115 + > 1116 + {pollingDomains.has(d.id) ? ( 1117 + <> 1118 + <Loader2 size={14} className="animate-spin" style={{ marginRight: 6 }} /> 1119 + Checking... 1120 + </> 1121 + ) : ( 1122 + 'Check' 1123 + )} 1124 </button> 1125 )} 1126 <button className="btn danger" onClick={() => removeCustomDomain(d.id)}>
+5
client/src/styles.css
··· 1719 } 1720 1721 @keyframes spin { 1722 to { 1723 transform: rotate(360deg); 1724 } ··· 1726 1727 .animate-spin { 1728 animation: spin 1s linear infinite; 1729 } 1730 1731 .sidebarUser {
··· 1719 } 1720 1721 @keyframes spin { 1722 + from { 1723 + transform: rotate(0deg); 1724 + } 1725 + 1726 to { 1727 transform: rotate(360deg); 1728 } ··· 1730 1731 .animate-spin { 1732 animation: spin 1s linear infinite; 1733 + display: inline-block; 1734 } 1735 1736 .sidebarUser {
+4
edge/worker.js
··· 82 const hostname = (request.headers.get('x-forwarded-host') || url.hostname).toLowerCase(); 83 const { ROOT_DOMAIN, B2_DOWNLOAD_BASE, B2_BUCKET_NAME, B2_KEY_ID, B2_APP_KEY, ROUTING } = env; 84 85 if (!B2_DOWNLOAD_BASE || !B2_BUCKET_NAME) { 86 return new Response('Service misconfigured', { status: 500 }); 87 }
··· 82 const hostname = (request.headers.get('x-forwarded-host') || url.hostname).toLowerCase(); 83 const { ROOT_DOMAIN, B2_DOWNLOAD_BASE, B2_BUCKET_NAME, B2_KEY_ID, B2_APP_KEY, ROUTING } = env; 84 85 + if (hostname === ROOT_DOMAIN) { 86 + return fetch(request); 87 + } 88 + 89 if (!B2_DOWNLOAD_BASE || !B2_BUCKET_NAME) { 90 return new Response('Service misconfigured', { status: 500 }); 91 }
+2 -4
edge/wrangler.toml
··· 5 # Enable workers.dev subdomain (needed for custom domain fallback origin) 6 workers_dev = true 7 8 - # Handle *.boop.cat subdomains 9 - # Custom domains use Cloudflare for SaaS and route through sites.boop.cat fallback 10 routes = [ 11 - { pattern = "*.boop.cat/*", zone_name = "boop.cat" }, 12 - { pattern = "sites.boop.cat", custom_domain = true } 13 ] 14 15 # Custom domain support - the fallback origin (sites.boop.cat) should CNAME to this worker
··· 5 # Enable workers.dev subdomain (needed for custom domain fallback origin) 6 workers_dev = true 7 8 + # Handle all traffic on the zone, including CF for SaaS custom hostnames 9 routes = [ 10 + { pattern = "*/*", zone_name = "boop.cat" } 11 ] 12 13 # Custom domain support - the fallback origin (sites.boop.cat) should CNAME to this worker