your personal website on atproto - mirror blento.app
at some-fixes 284 lines 8.5 kB view raw
1<script lang="ts" module> 2 export const customDomainModalState = $state({ 3 visible: false, 4 show: () => (customDomainModalState.visible = true), 5 hide: () => (customDomainModalState.visible = false) 6 }); 7</script> 8 9<script lang="ts"> 10 import { putRecord, getRecord, getHandleOrDid } from '$lib/atproto/methods'; 11 import { user } from '$lib/atproto'; 12 import { Button, Input } from '@foxui/core'; 13 import Modal from '$lib/components/modal/Modal.svelte'; 14 import { launchConfetti } from '@foxui/visual'; 15 16 let { publicationUrl }: { publicationUrl?: string } = $props(); 17 18 let currentDomain = $derived( 19 publicationUrl?.startsWith('https://') && !publicationUrl.includes('blento.app') 20 ? publicationUrl.replace('https://', '') 21 : '' 22 ); 23 24 let step: 'current' | 'input' | 'instructions' | 'verifying' | 'removing' | 'success' | 'error' = 25 $state('input'); 26 let domain = $state(''); 27 let errorMessage = $state(''); 28 let errorHint = $state(''); 29 30 $effect(() => { 31 if (customDomainModalState.visible) { 32 step = currentDomain ? 'current' : 'input'; 33 } else { 34 step = 'input'; 35 domain = ''; 36 errorMessage = ''; 37 errorHint = ''; 38 } 39 }); 40 41 async function removeDomain() { 42 step = 'removing'; 43 try { 44 const existing = await getRecord({ 45 collection: 'site.standard.publication', 46 rkey: 'blento.self' 47 }); 48 49 if (existing?.value) { 50 const { url: _url, ...rest } = existing.value as Record<string, unknown>; 51 await putRecord({ 52 collection: 'site.standard.publication', 53 rkey: 'blento.self', 54 record: rest 55 }); 56 } 57 58 step = 'input'; 59 } catch (err: unknown) { 60 errorMessage = err instanceof Error ? err.message : String(err); 61 step = 'error'; 62 } 63 } 64 65 function goToInstructions() { 66 if (!domain.trim()) return; 67 step = 'instructions'; 68 } 69 70 async function verify() { 71 step = 'verifying'; 72 try { 73 // Step 1: Verify DNS records 74 const dnsRes = await fetch('/api/verify-domain', { 75 method: 'POST', 76 headers: { 'Content-Type': 'application/json' }, 77 body: JSON.stringify({ domain }) 78 }); 79 80 const dnsData = await dnsRes.json(); 81 82 if (!dnsRes.ok || dnsData.error) { 83 errorMessage = dnsData.error; 84 errorHint = dnsData.hint || ''; 85 step = 'error'; 86 return; 87 } 88 89 // Step 2: Write URL to ATProto profile 90 const existing = await getRecord({ 91 collection: 'site.standard.publication', 92 rkey: 'blento.self' 93 }); 94 95 await putRecord({ 96 collection: 'site.standard.publication', 97 rkey: 'blento.self', 98 record: { 99 ...(existing?.value || {}), 100 url: 'https://' + domain 101 } 102 }); 103 104 // Step 3: Activate domain in KV (server verifies profile has the URL) 105 const activateRes = await fetch('/api/activate-domain', { 106 method: 'POST', 107 headers: { 'Content-Type': 'application/json' }, 108 body: JSON.stringify({ did: user.did, domain }) 109 }); 110 111 const activateData = await activateRes.json(); 112 113 if (!activateRes.ok || activateData.error) { 114 errorMessage = activateData.error; 115 errorHint = ''; 116 step = 'error'; 117 return; 118 } 119 120 // Refresh cached profile 121 if (user.profile) { 122 fetch(`/${getHandleOrDid(user.profile)}/api/refresh`).catch(() => {}); 123 } 124 125 launchConfetti(); 126 step = 'success'; 127 } catch (err: unknown) { 128 errorMessage = err instanceof Error ? err.message : String(err); 129 step = 'error'; 130 } 131 } 132 133 async function copyToClipboard(text: string) { 134 await navigator.clipboard.writeText(text); 135 } 136</script> 137 138<Modal bind:open={customDomainModalState.visible}> 139 {#if step === 'current'} 140 <h3 class="text-base-900 dark:text-base-100 font-semibold" id="custom-domain-modal-title"> 141 Custom Domain 142 </h3> 143 144 <div 145 class="bg-base-200 dark:bg-base-700 mt-2 flex items-center justify-between rounded-2xl px-3 py-2 font-mono text-sm" 146 > 147 <span>{currentDomain}</span> 148 </div> 149 150 <div class="mt-4 flex gap-2"> 151 <Button variant="ghost" onclick={removeDomain}>Remove</Button> 152 <Button variant="ghost" onclick={() => (step = 'input')}>Change</Button> 153 <Button onclick={() => customDomainModalState.hide()}>Close</Button> 154 </div> 155 {:else if step === 'input'} 156 <h3 class="text-base-900 dark:text-base-100 font-semibold" id="custom-domain-modal-title"> 157 Custom Domain 158 </h3> 159 160 <Input type="text" bind:value={domain} placeholder="mydomain.com" /> 161 162 <div class="mt-4 flex gap-2"> 163 <Button variant="ghost" onclick={() => customDomainModalState.hide()}>Cancel</Button> 164 <Button onclick={goToInstructions} disabled={!domain.trim()}>Next</Button> 165 </div> 166 {:else if step === 'instructions'} 167 <h3 class="text-base-900 dark:text-base-100 font-semibold" id="custom-domain-modal-title"> 168 Set up your domain 169 </h3> 170 171 <p class="text-base-800 dark:text-base-200 mt-2 text-sm"> 172 Add a CNAME record for your domain pointing to: 173 </p> 174 175 <div 176 class="bg-base-200 dark:bg-base-700 mt-2 flex items-center justify-between rounded-2xl px-3 py-2 font-mono text-sm" 177 > 178 <span>blento-proxy.fly.dev</span> 179 <button 180 class="text-base-600 hover:text-base-900 dark:text-base-400 dark:hover:text-base-100 ml-2 cursor-pointer" 181 onclick={() => copyToClipboard('blento-proxy.fly.dev')} 182 > 183 <svg 184 xmlns="http://www.w3.org/2000/svg" 185 fill="none" 186 viewBox="0 0 24 24" 187 stroke-width="1.5" 188 stroke="currentColor" 189 class="size-4" 190 > 191 <path 192 stroke-linecap="round" 193 stroke-linejoin="round" 194 d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9.75a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184" 195 /> 196 </svg> 197 <span class="sr-only">Copy to clipboard</span> 198 </button> 199 </div> 200 201 <div class="mt-4 flex gap-2"> 202 <Button variant="ghost" onclick={() => (step = 'input')}>Back</Button> 203 <Button onclick={verify}>Verify</Button> 204 </div> 205 {:else if step === 'verifying'} 206 <h3 class="text-base-900 dark:text-base-100 font-semibold" id="custom-domain-modal-title"> 207 Verifying... 208 </h3> 209 210 <p class="text-base-800 dark:text-base-200 mt-2 text-sm"> 211 Checking DNS records and verifying your domain. 212 </p> 213 214 <div class="mt-4 flex items-center gap-2"> 215 <svg 216 class="text-base-500 size-5 animate-spin" 217 xmlns="http://www.w3.org/2000/svg" 218 fill="none" 219 viewBox="0 0 24 24" 220 > 221 <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" 222 ></circle> 223 <path 224 class="opacity-75" 225 fill="currentColor" 226 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" 227 ></path> 228 </svg> 229 <span class="text-base-600 dark:text-base-400 text-sm">Verifying...</span> 230 </div> 231 {:else if step === 'removing'} 232 <h3 class="text-base-900 dark:text-base-100 font-semibold" id="custom-domain-modal-title"> 233 Removing... 234 </h3> 235 236 <div class="mt-4 flex items-center gap-2"> 237 <svg 238 class="text-base-500 size-5 animate-spin" 239 xmlns="http://www.w3.org/2000/svg" 240 fill="none" 241 viewBox="0 0 24 24" 242 > 243 <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" 244 ></circle> 245 <path 246 class="opacity-75" 247 fill="currentColor" 248 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" 249 ></path> 250 </svg> 251 <span class="text-base-600 dark:text-base-400 text-sm">Removing domain...</span> 252 </div> 253 {:else if step === 'success'} 254 <h3 class="text-base-900 dark:text-base-100 font-semibold" id="custom-domain-modal-title"> 255 Domain verified! 256 </h3> 257 258 <p class="text-base-800 dark:text-base-200 mt-2 text-sm"> 259 Your custom domain {domain} has been set up successfully. 260 </p> 261 262 <div class="mt-4"> 263 <Button onclick={() => customDomainModalState.hide()}>Close</Button> 264 </div> 265 {:else if step === 'error'} 266 <h3 class="text-base-900 dark:text-base-100 font-semibold" id="custom-domain-modal-title"> 267 Verification failed 268 </h3> 269 270 <p class="mt-2 text-sm text-red-500 dark:text-red-400"> 271 {errorMessage} 272 </p> 273 {#if errorHint} 274 <p class="mt-1 text-sm font-bold text-red-500 dark:text-red-400"> 275 {errorHint} 276 </p> 277 {/if} 278 279 <div class="mt-4 flex gap-2"> 280 <Button variant="ghost" onclick={() => customDomainModalState.hide()}>Close</Button> 281 <Button onclick={verify}>Retry</Button> 282 </div> 283 {/if} 284</Modal>