your personal website on atproto - mirror
blento.app
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>