your personal website on atproto - mirror blento.app
at funding 179 lines 5.4 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 } 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 step: 'input' | 'instructions' | 'verifying' | 'success' | 'error' = $state('input'); 17 let domain = $state(''); 18 let errorMessage = $state(''); 19 20 $effect(() => { 21 if (!customDomainModalState.visible) { 22 step = 'input'; 23 domain = ''; 24 errorMessage = ''; 25 } 26 }); 27 28 function goToInstructions() { 29 if (!domain.trim()) return; 30 step = 'instructions'; 31 } 32 33 async function verify() { 34 step = 'verifying'; 35 try { 36 const existing = await getRecord({ 37 collection: 'site.standard.publication', 38 rkey: 'blento.self' 39 }); 40 41 await putRecord({ 42 collection: 'site.standard.publication', 43 rkey: 'blento.self', 44 record: { 45 ...(existing?.value || {}), 46 url: 'https://' + domain 47 } 48 }); 49 50 const res = await fetch('/api/verify-domain', { 51 method: 'POST', 52 headers: { 'Content-Type': 'application/json' }, 53 body: JSON.stringify({ did: user.did, domain }) 54 }); 55 56 const data = await res.json(); 57 58 if (data.success) { 59 launchConfetti(); 60 step = 'success'; 61 } else if (data.error) { 62 errorMessage = data.error; 63 step = 'error'; 64 } 65 } catch (err: unknown) { 66 errorMessage = err instanceof Error ? err.message : String(err); 67 step = 'error'; 68 } 69 } 70 71 async function copyToClipboard(text: string) { 72 await navigator.clipboard.writeText(text); 73 } 74</script> 75 76<Modal bind:open={customDomainModalState.visible}> 77 {#if step === 'input'} 78 <h3 class="text-base-900 dark:text-base-100 font-semibold" id="custom-domain-modal-title"> 79 Custom Domain 80 </h3> 81 82 <Input type="text" bind:value={domain} placeholder="mydomain.com" /> 83 84 <div class="mt-4 flex gap-2"> 85 <Button variant="ghost" onclick={() => customDomainModalState.hide()}>Cancel</Button> 86 <Button onclick={goToInstructions} disabled={!domain.trim()}>Next</Button> 87 </div> 88 {:else if step === 'instructions'} 89 <h3 class="text-base-900 dark:text-base-100 font-semibold" id="custom-domain-modal-title"> 90 Set up your domain 91 </h3> 92 93 <p class="text-base-800 dark:text-base-200 mt-2 text-sm"> 94 Add a CNAME record for your domain pointing to: 95 </p> 96 97 <div 98 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" 99 > 100 <span>blento-proxy.fly.dev</span> 101 <button 102 class="text-base-600 hover:text-base-900 dark:text-base-400 dark:hover:text-base-100 ml-2 cursor-pointer" 103 onclick={() => copyToClipboard('blento-proxy.fly.dev')} 104 > 105 <svg 106 xmlns="http://www.w3.org/2000/svg" 107 fill="none" 108 viewBox="0 0 24 24" 109 stroke-width="1.5" 110 stroke="currentColor" 111 class="size-4" 112 > 113 <path 114 stroke-linecap="round" 115 stroke-linejoin="round" 116 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" 117 /> 118 </svg> 119 <span class="sr-only">Copy to clipboard</span> 120 </button> 121 </div> 122 123 <div class="mt-4 flex gap-2"> 124 <Button variant="ghost" onclick={() => (step = 'input')}>Back</Button> 125 <Button onclick={verify}>Verify</Button> 126 </div> 127 {:else if step === 'verifying'} 128 <h3 class="text-base-900 dark:text-base-100 font-semibold" id="custom-domain-modal-title"> 129 Verifying... 130 </h3> 131 132 <p class="text-base-800 dark:text-base-200 mt-2 text-sm"> 133 Checking DNS records and verifying your domain. 134 </p> 135 136 <div class="mt-4 flex items-center gap-2"> 137 <svg 138 class="text-base-500 size-5 animate-spin" 139 xmlns="http://www.w3.org/2000/svg" 140 fill="none" 141 viewBox="0 0 24 24" 142 > 143 <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" 144 ></circle> 145 <path 146 class="opacity-75" 147 fill="currentColor" 148 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" 149 ></path> 150 </svg> 151 <span class="text-base-600 dark:text-base-400 text-sm">Verifying...</span> 152 </div> 153 {:else if step === 'success'} 154 <h3 class="text-base-900 dark:text-base-100 font-semibold" id="custom-domain-modal-title"> 155 Domain verified! 156 </h3> 157 158 <p class="text-base-800 dark:text-base-200 mt-2 text-sm"> 159 Your custom domain {domain} has been set up successfully. 160 </p> 161 162 <div class="mt-4"> 163 <Button onclick={() => customDomainModalState.hide()}>Close</Button> 164 </div> 165 {:else if step === 'error'} 166 <h3 class="text-base-900 dark:text-base-100 font-semibold" id="custom-domain-modal-title"> 167 Verification failed 168 </h3> 169 170 <p class="mt-2 text-sm text-red-500 dark:text-red-400"> 171 {errorMessage} 172 </p> 173 174 <div class="mt-4 flex gap-2"> 175 <Button variant="ghost" onclick={() => customDomainModalState.hide()}>Close</Button> 176 <Button onclick={verify}>Retry</Button> 177 </div> 178 {/if} 179</Modal>