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 [wispDomains, setWispDomains] = useState<WispDomain[]>([])
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 setWispDomains(data.wispDomains || [])
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 (domain: string, 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({ domain, 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 deleteWispDomain = async (domain: string) => {
137 if (!confirm('Are you sure you want to remove this wisp.place domain?')) {
138 return false
139 }
140
141 try {
142 const response = await fetch(`/api/domain/wisp/${encodeURIComponent(domain)}`, {
143 method: 'DELETE'
144 })
145
146 const data = await response.json()
147 if (data.success) {
148 await fetchDomains()
149 return true
150 } else {
151 throw new Error('Failed to delete domain')
152 }
153 } catch (err) {
154 console.error('Delete wisp domain error:', err)
155 alert(
156 `Failed to delete domain: ${err instanceof Error ? err.message : 'Unknown error'}`
157 )
158 return false
159 }
160 }
161
162 const mapCustomDomain = async (domainId: string, siteRkey: string | null) => {
163 try {
164 const response = await fetch(`/api/domain/custom/${domainId}/map-site`, {
165 method: 'POST',
166 headers: { 'Content-Type': 'application/json' },
167 body: JSON.stringify({ siteRkey })
168 })
169 const data = await response.json()
170 if (!data.success) throw new Error(`Failed to map custom domain ${domainId}`)
171 return true
172 } catch (err) {
173 console.error('Map custom domain error:', err)
174 throw err
175 }
176 }
177
178 const claimWispDomain = async (handle: string) => {
179 try {
180 const response = await fetch('/api/domain/claim', {
181 method: 'POST',
182 headers: { 'Content-Type': 'application/json' },
183 body: JSON.stringify({ handle })
184 })
185
186 const data = await response.json()
187 if (data.success) {
188 await fetchDomains()
189 return { success: true }
190 } else {
191 throw new Error(data.error || 'Failed to claim domain')
192 }
193 } catch (err) {
194 console.error('Claim domain error:', err)
195 const errorMessage = err instanceof Error ? err.message : 'Unknown error'
196
197 // Handle domain limit error more gracefully
198 if (errorMessage.includes('Domain limit reached')) {
199 alert('You have already claimed 3 wisp.place subdomains (maximum limit).')
200 await fetchDomains()
201 } else {
202 alert(`Failed to claim domain: ${errorMessage}`)
203 }
204 return { success: false, error: errorMessage }
205 }
206 }
207
208 const checkWispAvailability = async (handle: string) => {
209 const trimmedHandle = handle.trim().toLowerCase()
210 if (!trimmedHandle) {
211 return { available: null }
212 }
213
214 try {
215 const response = await fetch(`/api/domain/check?handle=${encodeURIComponent(trimmedHandle)}`)
216 const data = await response.json()
217 return { available: data.available }
218 } catch (err) {
219 console.error('Check availability error:', err)
220 return { available: false }
221 }
222 }
223
224 return {
225 wispDomains,
226 customDomains,
227 domainsLoading,
228 verificationStatus,
229 fetchDomains,
230 addCustomDomain,
231 verifyDomain,
232 deleteCustomDomain,
233 mapWispDomain,
234 deleteWispDomain,
235 mapCustomDomain,
236 claimWispDomain,
237 checkWispAvailability
238 }
239}