Skip to content
File

Blob: ui/client/CustomDomains.svelte

4.3 KB
1<script lang="ts">
2 import { onDestroy } from "svelte";
3 import Button from "../shared/Button.svelte";
4 import { INPUT_CLASSES } from "../shared/styles";
5 import { createAPI, errorMessage, type DomainRecord } from "./api";
6 
7 type DomainAction = "acme" | "validate";
8 type DomainResult = DomainRecord & { domain: string; verified: boolean };
9 
10 let domain = $state("");
11 let pending = $state<DomainAction | "">("");
12 let requestedDomain = $state("");
13 let output = $state<DomainResult | null>(null);
14 let error = $state("");
15 let domainRevision = 0;
16 const api = createAPI();
17 
18 function editDomain() {
19 domainRevision += 1;
20 output = null;
21 error = "";
22 }
23 
24 async function domainAction(action: DomainAction) {
25 if (pending) return;
26 const submittedDomain = domain.trim();
27 if (!submittedDomain || /[\s/:]/.test(submittedDomain)) {
28 error = "Enter a domain without a protocol or path.";
29 return;
30 }
31 const revision = domainRevision;
32 pending = action;
33 requestedDomain = submittedDomain;
34 error = "";
35 output = null;
36 try {
37 const record = await api.request<DomainRecord>(`/api/${action}/${encodeURIComponent(submittedDomain)}`);
38 if (revision === domainRevision)
39 output = { ...record, domain: submittedDomain, verified: action === "validate" };
40 } catch (cause) {
41 if (revision === domainRevision) error = `${submittedDomain}: ${errorMessage(cause)}`;
42 } finally {
43 pending = "";
44 requestedDomain = "";
45 }
46 }
47 
48 onDestroy(api.abort);
49</script>
50 
51<section id="domains" class="min-w-0 scroll-mt-6" aria-labelledby="domains-heading">
52 <h2 id="domains-heading" class="mb-3.5 text-lg leading-snug font-semibold tracking-tight">Custom domains</h2>
53 <form onsubmit={(event) => { event.preventDefault(); void domainAction("acme"); }}>
54 <label for="domain" class="mb-1.5 block font-medium">Domain</label>
55 <input
56 id="domain"
57 name="domain"
58 type="text"
59 class={INPUT_CLASSES}
60 bind:value={domain}
61 oninput={editDomain}
62 placeholder="app.example.com"
63 autocomplete="off"
64 autocapitalize="none"
65 spellcheck="false"
66 maxlength="253"
67 required
68 aria-describedby="domain-hint domain-feedback"
69 aria-invalid={Boolean(error)}
70 />
71 <p id="domain-hint" class="mt-1.5 mb-2.5 text-[0.8125rem] text-muted">Add the record in DNS, then verify.</p>
72 <div class="flex flex-wrap items-center gap-2">
73 <Button type="submit" disabled={Boolean(pending) || !domain.trim()}>{pending === "acme" ? "Getting record…" : "Get DNS record"}</Button>
74 <Button disabled={Boolean(pending) || !domain.trim()} onclick={() => domainAction("validate")}>{pending === "validate" ? "Verifying…" : "Verify ownership"}</Button>
75 </div>
76 </form>
77 
78 <div id="domain-feedback" aria-live="polite" aria-busy={Boolean(pending)}>
79 {#if pending}
80 <p class="my-2.5 rounded-md border border-line bg-chrome px-3 py-2 wrap-anywhere">
81 {pending === "validate" ? "Verifying" : "Fetching"} <span class="font-mono text-[0.8125rem]">{requestedDomain}</span>…
82 </p>
83 {/if}
84 {#if error}
85 <div class="my-2.5 rounded-md border border-danger/40 bg-chrome px-3 py-2 text-danger wrap-anywhere">
86 {#if error.length > 120}
87 <details><summary class="cursor-pointer py-1 font-medium text-zinc-200">Domain request failed</summary><p class="mt-2">{error}</p></details>
88 {:else}
89 {error}
90 {/if}
91 </div>
92 {/if}
93 {#if output}
94 <div class="mt-3.5 border-t border-line pt-3">
95 <h3 class={['text-[0.9375rem] font-semibold wrap-anywhere', output.verified && 'text-success']}>
96 {output.verified ? "Verified" : "DNS record"} <span class="font-mono text-[0.8125rem]">{output.domain}</span>
97 </h3>
98 <dl class="mt-3 [&>div]:grid [&>div]:grid-cols-[4rem_minmax(0,1fr)] [&>div]:gap-4 [&>div]:py-1.5 [&_dd]:font-mono [&_dd]:text-[0.8125rem] [&_dd]:wrap-anywhere [&_dt]:text-muted">
99 <div><dt>Name</dt><dd>{output.record}</dd></div>
100 <div><dt>Type</dt><dd>{output.type}</dd></div>
101 <div><dt>Value</dt><dd>{output.content}</dd></div>
102 </dl>
103 {#if output.verified}<a href="#configuration" class="text-accent-300 underline underline-offset-4 hover:text-accent-200">Add to configuration</a>{/if}
104 </div>
105 {/if}
106 </div>
107</section>