Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
1import { useState } from 'react' 2 3export interface CustomDomain { 4 id: string 5 domain: string 6 did: string 7 rkey: string 8 verified: boolean 9 last_verified_at: number | null 10 created_at: number 11} 12 13export interface WispDomain { 14 domain: string 15 rkey: string | null 16} 17 18type VerificationStatus = 'idle' | 'verifying' | 'success' | 'error' 19 20export function useDomainData() { 21 const [wispDomain, setWispDomain] = useState<WispDomain | null>(null) 22 const [customDomains, setCustomDomains] = useState<CustomDomain[]>([]) 23 const [domainsLoading, setDomainsLoading] = useState(true) 24 const [verificationStatus, setVerificationStatus] = useState<{ 25 [id: string]: VerificationStatus 26 }>({}) 27 28 const fetchDomains = async () => { 29 try { 30 const response = await fetch('/api/user/domains') 31 const data = await response.json() 32 setWispDomain(data.wispDomain) 33 setCustomDomains(data.customDomains || []) 34 } catch (err) { 35 console.error('Failed to fetch domains:', err) 36 } finally { 37 setDomainsLoading(false) 38 } 39 } 40 41 const addCustomDomain = async (domain: string) => { 42 try { 43 const response = await fetch('/api/domain/custom/add', { 44 method: 'POST', 45 headers: { 'Content-Type': 'application/json' }, 46 body: JSON.stringify({ domain }) 47 }) 48 49 const data = await response.json() 50 if (data.success) { 51 await fetchDomains() 52 return { success: true, id: data.id } 53 } else { 54 throw new Error(data.error || 'Failed to add domain') 55 } 56 } catch (err) { 57 console.error('Add domain error:', err) 58 alert( 59 `Failed to add domain: ${err instanceof Error ? err.message : 'Unknown error'}` 60 ) 61 return { success: false } 62 } 63 } 64 65 const verifyDomain = async (id: string) => { 66 setVerificationStatus({ ...verificationStatus, [id]: 'verifying' }) 67 68 try { 69 const response = await fetch('/api/domain/custom/verify', { 70 method: 'POST', 71 headers: { 'Content-Type': 'application/json' }, 72 body: JSON.stringify({ id }) 73 }) 74 75 const data = await response.json() 76 if (data.success && data.verified) { 77 setVerificationStatus({ ...verificationStatus, [id]: 'success' }) 78 await fetchDomains() 79 } else { 80 setVerificationStatus({ ...verificationStatus, [id]: 'error' }) 81 if (data.error) { 82 alert(`Verification failed: ${data.error}`) 83 } 84 } 85 } catch (err) { 86 console.error('Verify domain error:', err) 87 setVerificationStatus({ ...verificationStatus, [id]: 'error' }) 88 alert( 89 `Verification failed: ${err instanceof Error ? err.message : 'Unknown error'}` 90 ) 91 } 92 } 93 94 const deleteCustomDomain = async (id: string) => { 95 if (!confirm('Are you sure you want to remove this custom domain?')) { 96 return false 97 } 98 99 try { 100 const response = await fetch(`/api/domain/custom/${id}`, { 101 method: 'DELETE' 102 }) 103 104 const data = await response.json() 105 if (data.success) { 106 await fetchDomains() 107 return true 108 } else { 109 throw new Error('Failed to delete domain') 110 } 111 } catch (err) { 112 console.error('Delete domain error:', err) 113 alert( 114 `Failed to delete domain: ${err instanceof Error ? err.message : 'Unknown error'}` 115 ) 116 return false 117 } 118 } 119 120 const mapWispDomain = async (siteRkey: string | null) => { 121 try { 122 const response = await fetch('/api/domain/wisp/map-site', { 123 method: 'POST', 124 headers: { 'Content-Type': 'application/json' }, 125 body: JSON.stringify({ siteRkey }) 126 }) 127 const data = await response.json() 128 if (!data.success) throw new Error('Failed to map wisp domain') 129 return true 130 } catch (err) { 131 console.error('Map wisp domain error:', err) 132 throw err 133 } 134 } 135 136 const mapCustomDomain = async (domainId: string, siteRkey: string | null) => { 137 try { 138 const response = await fetch(`/api/domain/custom/${domainId}/map-site`, { 139 method: 'POST', 140 headers: { 'Content-Type': 'application/json' }, 141 body: JSON.stringify({ siteRkey }) 142 }) 143 const data = await response.json() 144 if (!data.success) throw new Error(`Failed to map custom domain ${domainId}`) 145 return true 146 } catch (err) { 147 console.error('Map custom domain error:', err) 148 throw err 149 } 150 } 151 152 const claimWispDomain = async (handle: string) => { 153 try { 154 const response = await fetch('/api/domain/claim', { 155 method: 'POST', 156 headers: { 'Content-Type': 'application/json' }, 157 body: JSON.stringify({ handle }) 158 }) 159 160 const data = await response.json() 161 if (data.success) { 162 await fetchDomains() 163 return { success: true } 164 } else { 165 throw new Error(data.error || 'Failed to claim domain') 166 } 167 } catch (err) { 168 console.error('Claim domain error:', err) 169 const errorMessage = err instanceof Error ? err.message : 'Unknown error' 170 171 // Handle "Already claimed" error more gracefully 172 if (errorMessage.includes('Already claimed')) { 173 alert('You have already claimed a wisp.place subdomain. Please refresh the page.') 174 await fetchDomains() 175 } else { 176 alert(`Failed to claim domain: ${errorMessage}`) 177 } 178 return { success: false, error: errorMessage } 179 } 180 } 181 182 const checkWispAvailability = async (handle: string) => { 183 const trimmedHandle = handle.trim().toLowerCase() 184 if (!trimmedHandle) { 185 return { available: null } 186 } 187 188 try { 189 const response = await fetch(`/api/domain/check?handle=${encodeURIComponent(trimmedHandle)}`) 190 const data = await response.json() 191 return { available: data.available } 192 } catch (err) { 193 console.error('Check availability error:', err) 194 return { available: false } 195 } 196 } 197 198 return { 199 wispDomain, 200 customDomains, 201 domainsLoading, 202 verificationStatus, 203 fetchDomains, 204 addCustomDomain, 205 verifyDomain, 206 deleteCustomDomain, 207 mapWispDomain, 208 mapCustomDomain, 209 claimWispDomain, 210 checkWispAvailability 211 } 212}