chore: improve ui experience of custom domain setup

This commit is contained in:
Rotimi Best
2026-05-21 11:30:12 +01:00
parent 61cbb258ea
commit c1bbd02f8e
12 changed files with 209 additions and 77 deletions
@@ -1,10 +1,13 @@
<script lang="ts"> <script lang="ts">
import AlertTriangleIcon from '@lucide/svelte/icons/alert-triangle';
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right'; import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
import CheckIcon from '@lucide/svelte/icons/check';
import ClockIcon from '@lucide/svelte/icons/clock';
import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw'; import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
import ShieldCheckIcon from '@lucide/svelte/icons/shield-check';
import TrashIcon from '@lucide/svelte/icons/trash'; import TrashIcon from '@lucide/svelte/icons/trash';
import type { DomainRequestData, DomainRequestStatus } from '$features/org/utils/types'; import type { DomainRequestData, DomainRequestStatus } from '$features/org/utils/types';
import isValidDomain from 'is-valid-domain'; import isValidDomain from 'is-valid-domain';
import { parse as parseDomain } from 'tldts';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onMount, untrack } from 'svelte'; import { onMount, untrack } from 'svelte';
@@ -18,6 +21,9 @@
import { BRAND_ROOT_DOMAIN, TENANT_ROOT_DOMAIN } from '@cio/utils/constants'; import { BRAND_ROOT_DOMAIN, TENANT_ROOT_DOMAIN } from '@cio/utils/constants';
import { Badge } from '@cio/ui/base/badge'; import { Badge } from '@cio/ui/base/badge';
import { IconButton } from '@cio/ui/custom/icon-button';
import { Spinner } from '@cio/ui/base/spinner';
import { Button } from '@cio/ui/base/button'; import { Button } from '@cio/ui/base/button';
import { CopyButton } from '@cio/ui/base/copy-button'; import { CopyButton } from '@cio/ui/base/copy-button';
import { Textarea } from '@cio/ui/base/textarea'; import { Textarea } from '@cio/ui/base/textarea';
@@ -33,6 +39,7 @@
let isCustomDomainLoading = $state(false); let isCustomDomainLoading = $state(false);
let isRefreshing = $state(false); let isRefreshing = $state(false);
let domainSetup = $state<DomainRequestData | null>(null); let domainSetup = $state<DomainRequestData | null>(null);
let domainApex = $state<string | null>(null);
const isDomainValid = $derived(isValidDomain(sanitizeDomain(customDomain), { subdomain: true })); const isDomainValid = $derived(isValidDomain(sanitizeDomain(customDomain), { subdomain: true }));
const currentDomainStatus = $derived<DomainRequestStatus | null>( const currentDomainStatus = $derived<DomainRequestStatus | null>(
@@ -40,15 +47,20 @@
($currentOrg.customDomain ? ($currentOrg.isCustomDomainVerified ? 'verified' : 'pending_verification') : null) ($currentOrg.customDomain ? ($currentOrg.isCustomDomainVerified ? 'verified' : 'pending_verification') : null)
); );
async function refreshDomainApex(hostname: string | null | undefined) {
if (!hostname) {
domainApex = null;
return;
}
const { parse } = await import('tldts/dist/es6/index.js');
domainApex = parse(hostname).domain ?? null;
}
function stripZone(recordName: string): string { function stripZone(recordName: string): string {
const hostname = $currentOrg.customDomain; if (!domainApex) return recordName;
if (!hostname) return recordName;
const apex = parseDomain(hostname).domain; const suffix = '.' + domainApex;
if (!apex) return recordName; if (recordName === domainApex) return recordName;
const suffix = '.' + apex;
if (recordName === apex) return recordName;
if (recordName.endsWith(suffix)) return recordName.slice(0, -suffix.length); if (recordName.endsWith(suffix)) return recordName.slice(0, -suffix.length);
return recordName; return recordName;
@@ -102,12 +114,14 @@
if (data.status === 'removed') { if (data.status === 'removed') {
$currentOrg.customDomain = ''; $currentOrg.customDomain = '';
$currentOrg.isCustomDomainVerified = false; $currentOrg.isCustomDomainVerified = false;
domainApex = null;
return; return;
} }
$currentOrg.customDomain = data.hostname; $currentOrg.customDomain = data.hostname;
$currentOrg.isCustomDomainVerified = data.verified; $currentOrg.isCustomDomainVerified = data.verified;
customDomain = ''; customDomain = '';
void refreshDomainApex(data.hostname);
if (data.verified && !wasVerified) { if (data.verified && !wasVerified) {
snackbar.success('components.settings.domains.verified_snackbar'); snackbar.success('components.settings.domains.verified_snackbar');
@@ -282,6 +296,7 @@
onMount(() => { onMount(() => {
if ($currentOrg.customDomain) { if ($currentOrg.customDomain) {
void refreshDomainApex($currentOrg.customDomain);
void handleRefreshCustomDomain(true); void handleRefreshCustomDomain(true);
} }
}); });
@@ -332,7 +347,13 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<p class="text-md flex items-center gap-2 font-medium"> <p class="text-md flex items-center gap-2 font-medium">
{$currentOrg.customDomain} {$currentOrg.customDomain}
<Button variant="outline" size="icon-sm" onclick={() => goto(`https://${$currentOrg.customDomain}`)}> <Button
variant="outline"
size="icon-sm"
onclick={() => {
window.open(`https://${$currentOrg.customDomain}`, '_blank');
}}
>
<ArrowUpRightIcon size={16} /> <ArrowUpRightIcon size={16} />
</Button> </Button>
</p> </p>
@@ -350,40 +371,119 @@
<Badge variant="default" class={`px-3 text-xs ${getDomainStatusClass(currentDomainStatus)}`}> <Badge variant="default" class={`px-3 text-xs ${getDomainStatusClass(currentDomainStatus)}`}>
{getDomainStatusLabel(currentDomainStatus)} {getDomainStatusLabel(currentDomainStatus)}
</Badge> </Badge>
<IconButton onclick={handleRemoveCustomDomain} loading={isCustomDomainLoading}>
{#if isCustomDomainLoading}
<Spinner class="ui:text-muted-foreground size-4" />
{:else}
<TrashIcon size={16} />
{/if}
</IconButton>
</div> </div>
</Field.Field> </Field.Field>
<Field.Description>{getDomainStatusDescription(currentDomainStatus)}</Field.Description> <Field.Description>{getDomainStatusDescription(currentDomainStatus)}</Field.Description>
{#if domainSetup?.validationErrors?.length}
<Field.Field>
<div
class="flex items-start gap-3 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm dark:border-red-900 dark:bg-red-950/40"
>
<AlertTriangleIcon class="mt-0.5 size-4 shrink-0 text-red-600 dark:text-red-400" />
<div class="min-w-0 flex-1">
<p class="font-medium text-red-900 dark:text-red-200">
{$t('components.settings.domains.validation_errors_heading')}
</p>
<ul class="mt-1 space-y-1 text-red-800 dark:text-red-300">
{#each domainSetup.validationErrors as message, i (i)}
<li>{message}</li>
{/each}
</ul>
</div>
</div>
</Field.Field>
{/if}
{#if currentDomainStatus === 'pending_verification'}
<Field.Field>
<div
class="flex items-start gap-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm dark:border-amber-900 dark:bg-amber-950/40"
>
<ShieldCheckIcon class="mt-0.5 size-4 shrink-0 text-amber-700 dark:text-amber-400" />
<div class="min-w-0 flex-1">
<p class="font-medium text-amber-900 dark:text-amber-200">
{$t('components.settings.domains.ssl_pending_heading')}
</p>
<p class="mt-1 text-amber-800 dark:text-amber-300">
{$t('components.settings.domains.ssl_pending_body')}
</p>
</div>
</div>
</Field.Field>
{/if}
{#if currentDomainStatus !== 'verified' && domainSetup?.dnsRecords?.length} {#if currentDomainStatus !== 'verified' && domainSetup?.dnsRecords?.length}
<Field.Field> <Field.Field>
<div class="flex flex-col gap-3"> <div class="ui:bg-card overflow-hidden rounded-lg border">
{#each domainSetup.dnsRecords as record, i (record.name + i)} {#each domainSetup.dnsRecords as record, i (record.name + i)}
{@const rows = [ {@const fields = [
{ label: $t('components.settings.domains.dns_type'), value: record.type },
{ label: $t('components.settings.domains.dns_name'), value: stripZone(record.name) }, { label: $t('components.settings.domains.dns_name'), value: stripZone(record.name) },
{ label: $t('components.settings.domains.dns_value'), value: record.value } { label: $t('components.settings.domains.dns_value'), value: record.value }
]} ]}
<div class="ui:bg-card overflow-hidden rounded-lg border"> <div class={'p-4 ' + (i > 0 ? 'border-t' : '')}>
<div <div class="mb-3 flex items-center gap-2">
class="ui:bg-muted/40 ui:text-muted-foreground border-b px-4 py-2 text-xs font-medium tracking-wide uppercase" <span
> class={'inline-flex items-center rounded-md px-2 py-0.5 font-mono text-xs font-semibold tracking-wide ' +
{$t('components.settings.domains.dns_record')} (record.type === 'CNAME'
? 'bg-blue-50 text-blue-700 ring-1 ring-blue-200 dark:bg-blue-950/40 dark:text-blue-300 dark:ring-blue-900'
: 'bg-slate-100 text-slate-700 ring-1 ring-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:ring-slate-700')}
>
{record.type}
</span>
<span class="ui:text-muted-foreground text-xs">
Record {i + 1} of {domainSetup.dnsRecords.length}
</span>
<span
class={'ml-auto inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium ' +
(record.status === 'active'
? 'bg-emerald-50 text-emerald-700 ring-1 ring-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-300 dark:ring-emerald-900'
: 'bg-amber-50 text-amber-800 ring-1 ring-amber-200 dark:bg-amber-950/40 dark:text-amber-300 dark:ring-amber-900')}
aria-label={record.status === 'active'
? $t('components.settings.domains.record_active')
: $t('components.settings.domains.record_pending')}
>
{#if record.status === 'active'}
<CheckIcon class="size-3" />
<span>{$t('components.settings.domains.record_active')}</span>
{:else}
<ClockIcon class="size-3" />
<span>{$t('components.settings.domains.record_pending')}...</span>
{/if}
</span>
</div> </div>
<dl class="divide-y"> <div class="flex flex-wrap gap-x-6 gap-y-3">
{#each rows as row (row.label)} {#each fields as field (field.label)}
<div class="grid grid-cols-[80px,1fr,auto] items-center gap-3 px-4 py-2.5"> <div class="min-w-[240px] flex-1">
<dt class="ui:text-muted-foreground text-xs font-medium tracking-wide uppercase"> <div class="ui:text-muted-foreground mb-1 text-xs font-medium tracking-wide uppercase">
{row.label} {field.label}
</dt> </div>
<dd class="min-w-0"> <div class="flex items-center gap-2">
<span class="block truncate font-mono text-sm">{row.value}</span> <span class="min-w-0 flex-1 truncate font-mono text-sm" title={field.value}
</dd> >{field.value}</span
<CopyButton text={row.value} variant="ghost" size="icon-sm" aria-label={'Copy ' + row.label} /> >
<CopyButton
text={field.value}
variant="outline"
size="icon-sm"
aria-label={'Copy ' + field.label}
class="shrink-0"
/>
</div>
</div> </div>
{/each} {/each}
</dl> </div>
</div> </div>
{/each} {/each}
</div> </div>
@@ -407,6 +507,7 @@
</Button> </Button>
{:else if currentDomainStatus !== 'verified'} {:else if currentDomainStatus !== 'verified'}
<Button <Button
variant="outline"
class="flex items-center gap-2 py-2" class="flex items-center gap-2 py-2"
onclick={() => handleRefreshCustomDomain()} onclick={() => handleRefreshCustomDomain()}
loading={isRefreshing} loading={isRefreshing}
@@ -416,19 +517,18 @@
{/if} {/if}
{$t('components.settings.domains.refresh')} {$t('components.settings.domains.refresh')}
</Button> </Button>
<Button
variant="destructive"
class="flex items-center gap-2 py-2"
onclick={handleRemoveCustomDomain}
loading={isCustomDomainLoading}
>
{#if !isCustomDomainLoading}
<TrashIcon size={16} />
{/if}
{$t('components.settings.domains.remove')}
</Button>
{/if} {/if}
<Button
variant="outline"
class="ui:text-destructive ui:hover:text-destructive ui:hover:bg-destructive/10 flex items-center gap-2 py-2"
onclick={handleRemoveCustomDomain}
loading={isCustomDomainLoading}
>
{#if !isCustomDomainLoading}
<TrashIcon size={16} />
{/if}
{$t('components.settings.domains.remove')}
</Button>
</Field.Field> </Field.Field>
{:else} {:else}
<Field.Field> <Field.Field>
@@ -2164,9 +2164,7 @@
"depth": "Hvor dybt skal kurset gå?", "depth": "Hvor dybt skal kurset gå?",
"topic": "Emne eller niche", "topic": "Emne eller niche",
"expertise_level": "Mål ekspertiseniveau", "expertise_level": "Mål ekspertiseniveau",
"documentation_url": { "documentation_url": "Dokumentations-URL (valgfrit)"
"hint": "Vi læser dette websted for at basere kurset på dit rigtige produkt."
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "Bliv ekspert på X", "title": "Bliv ekspert på X",
@@ -2419,7 +2417,12 @@
"error_description": "Dette tilpassede domæne eksisterer, men bekræftelsen er ikke fuldført endnu.", "error_description": "Dette tilpassede domæne eksisterer, men bekræftelsen er ikke fuldført endnu.",
"page_subtitle": "Run your public learning site on a web address you own.", "page_subtitle": "Run your public learning site on a web address you own.",
"dns_record": "DNS-record", "dns_record": "DNS-record",
"verified_snackbar": "Tilpasset domæne bekræftet" "verified_snackbar": "Tilpasset domæne bekræftet",
"record_active": "Opdaget",
"record_pending": "Afventer",
"ssl_pending_heading": "SSL-certifikat er ved at blive udstedt",
"ssl_pending_body": "Ejerskabet af dit domæne er bekræftet. Vi venter nu på, at Cloudflare udsteder og implementerer SSL-certifikatet. Dette tager normalt et par minutter, når alle nedenstående DNS-poster er på plads.",
"validation_errors_heading": "Certifikatudstedelse er blokeret"
} }
}, },
"quiz": { "quiz": {
@@ -2164,9 +2164,7 @@
"depth": "Wie tief soll der Kurs gehen?", "depth": "Wie tief soll der Kurs gehen?",
"topic": "Thema oder Nische", "topic": "Thema oder Nische",
"expertise_level": "Zielkompetenzniveau", "expertise_level": "Zielkompetenzniveau",
"documentation_url": { "documentation_url": "Dokumentations-URL (optional)"
"hint": "Wir werden diese Website lesen, um den Kurs in Ihrem realen Produkt zu verankern."
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "Werden Sie Experte für X", "title": "Werden Sie Experte für X",
@@ -2419,7 +2417,12 @@
"error_description": "Diese benutzerdefinierte Domäne existiert, aber die Überprüfung ist noch nicht abgeschlossen.", "error_description": "Diese benutzerdefinierte Domäne existiert, aber die Überprüfung ist noch nicht abgeschlossen.",
"page_subtitle": "Run your public learning site on a web address you own.", "page_subtitle": "Run your public learning site on a web address you own.",
"dns_record": "DNS-Eintrag", "dns_record": "DNS-Eintrag",
"verified_snackbar": "Benutzerdefinierte Domain verifiziert" "verified_snackbar": "Benutzerdefinierte Domain verifiziert",
"record_active": "Erkannt",
"record_pending": "Ausstehend",
"ssl_pending_heading": "SSL-Zertifikat wird ausgestellt",
"ssl_pending_body": "Der Besitz Ihrer Domain wird bestätigt. Wir warten nun darauf, dass Cloudflare das SSL-Zertifikat ausstellt und bereitstellt. Dies dauert normalerweise einige Minuten, sobald alle unten aufgeführten DNS-Einträge vorhanden sind.",
"validation_errors_heading": "Die Ausstellung des Zertifikats ist gesperrt"
} }
}, },
"quiz": { "quiz": {
@@ -1524,6 +1524,11 @@
"dns_value": "Value", "dns_value": "Value",
"dns_name": "Name", "dns_name": "Name",
"dns_record": "DNS record", "dns_record": "DNS record",
"record_active": "Detected",
"record_pending": "Pending",
"ssl_pending_heading": "SSL certificate is being issued",
"ssl_pending_body": "Ownership of your domain is verified. We're now waiting for Cloudflare to issue and deploy the SSL certificate. This usually takes a few minutes once all DNS records below are in place.",
"validation_errors_heading": "Certificate issuance is blocked",
"refresh": "Refresh", "refresh": "Refresh",
"reconnect": "Reconnect domain", "reconnect": "Reconnect domain",
"reconnect_hint": "Reconnect this domain to generate new Cloudflare setup records.", "reconnect_hint": "Reconnect this domain to generate new Cloudflare setup records.",
@@ -2164,9 +2164,7 @@
"depth": "¿Qué profundidad debe tener el curso?", "depth": "¿Qué profundidad debe tener el curso?",
"topic": "Tema o nicho", "topic": "Tema o nicho",
"expertise_level": "Nivel de experiencia objetivo", "expertise_level": "Nivel de experiencia objetivo",
"documentation_url": { "documentation_url": "URL de documentación (opcional)"
"hint": "Leeremos este sitio para basar el curso en su producto real."
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "Conviértete en un experto en X", "title": "Conviértete en un experto en X",
@@ -2419,7 +2417,12 @@
"error_description": "Este dominio personalizado existe, pero la verificación aún no se ha completado.", "error_description": "Este dominio personalizado existe, pero la verificación aún no se ha completado.",
"page_subtitle": "Run your public learning site on a web address you own.", "page_subtitle": "Run your public learning site on a web address you own.",
"dns_record": "registro DNS", "dns_record": "registro DNS",
"verified_snackbar": "Dominio personalizado verificado" "verified_snackbar": "Dominio personalizado verificado",
"record_active": "Detectado",
"record_pending": "Pendiente",
"ssl_pending_heading": "Se está emitiendo el certificado SSL",
"ssl_pending_body": "Se verifica la propiedad de su dominio. Ahora estamos esperando que Cloudflare emita e implemente el certificado SSL. Esto suele tardar unos minutos una vez que todos los registros DNS siguientes estén en su lugar.",
"validation_errors_heading": "La emisión de certificados está bloqueada"
} }
}, },
"quiz": { "quiz": {
@@ -2164,9 +2164,7 @@
"depth": "Jusqu’où doit aller le cours ?", "depth": "Jusqu’où doit aller le cours ?",
"topic": "Sujet ou niche", "topic": "Sujet ou niche",
"expertise_level": "Niveau d’expertise cible", "expertise_level": "Niveau d’expertise cible",
"documentation_url": { "documentation_url": "URL de la documentation (facultatif)"
"hint": "Nous lirons ce site pour ancrer le cours dans votre produit réel."
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "Devenez un expert sur X", "title": "Devenez un expert sur X",
@@ -2419,7 +2417,12 @@
"error_description": "Ce domaine personnalisé existe, mais la vérification n'est pas encore terminée.", "error_description": "Ce domaine personnalisé existe, mais la vérification n'est pas encore terminée.",
"page_subtitle": "Run your public learning site on a web address you own.", "page_subtitle": "Run your public learning site on a web address you own.",
"dns_record": "Enregistrement DNS", "dns_record": "Enregistrement DNS",
"verified_snackbar": "Domaine personnalisé vérifié" "verified_snackbar": "Domaine personnalisé vérifié",
"record_active": "Détecté",
"record_pending": "En attente",
"ssl_pending_heading": "Le certificat SSL est en cours d'émission",
"ssl_pending_body": "La propriété de votre domaine est vérifiée. Nous attendons maintenant que Cloudflare émette et déploie le certificat SSL. Cela prend généralement quelques minutes une fois que tous les enregistrements DNS ci-dessous sont en place.",
"validation_errors_heading": "La délivrance du certificat est bloquée"
} }
}, },
"quiz": { "quiz": {
@@ -2164,9 +2164,7 @@
"depth": "पाठ्यक्रम कितना गहरा होना चाहिए?", "depth": "पाठ्यक्रम कितना गहरा होना चाहिए?",
"topic": "विषय या आला", "topic": "विषय या आला",
"expertise_level": "लक्ष्य विशेषज्ञता स्तर", "expertise_level": "लक्ष्य विशेषज्ञता स्तर",
"documentation_url": { "documentation_url": "दस्तावेज़ीकरण URL (वैकल्पिक)"
"hint": "हम आपके वास्तविक उत्पाद में पाठ्यक्रम को आधार बनाने के लिए इस साइट को पढ़ेंगे।"
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "एक्स पर विशेषज्ञ बनें", "title": "एक्स पर विशेषज्ञ बनें",
@@ -2419,7 +2417,12 @@
"error_description": "यह कस्टम डोमेन मौजूद है, लेकिन सत्यापन अभी तक पूरा नहीं हुआ है।", "error_description": "यह कस्टम डोमेन मौजूद है, लेकिन सत्यापन अभी तक पूरा नहीं हुआ है।",
"page_subtitle": "Run your public learning site on a web address you own.", "page_subtitle": "Run your public learning site on a web address you own.",
"dns_record": "डीएनएस रिकॉर्ड", "dns_record": "डीएनएस रिकॉर्ड",
"verified_snackbar": "कस्टम डोमेन सत्यापित" "verified_snackbar": "कस्टम डोमेन सत्यापित",
"record_active": "पता चला",
"record_pending": "लंबित",
"ssl_pending_heading": "एसएसएल प्रमाणपत्र जारी किया जा रहा है",
"ssl_pending_body": "आपके डोमेन का स्वामित्व सत्यापित है. अब हम क्लाउडफ्लेयर द्वारा एसएसएल प्रमाणपत्र जारी करने और तैनात करने की प्रतीक्षा कर रहे हैं। नीचे दिए गए सभी DNS रिकॉर्ड सही जगह पर आ जाने पर आमतौर पर इसमें कुछ मिनट लगते हैं।",
"validation_errors_heading": "प्रमाणपत्र जारी करना अवरुद्ध है"
} }
}, },
"quiz": { "quiz": {
@@ -2164,9 +2164,7 @@
"depth": "Jak głęboko powinien sięgać kurs?", "depth": "Jak głęboko powinien sięgać kurs?",
"topic": "Temat lub nisza", "topic": "Temat lub nisza",
"expertise_level": "Docelowy poziom wiedzy", "expertise_level": "Docelowy poziom wiedzy",
"documentation_url": { "documentation_url": "Adres URL dokumentacji (opcjonalnie)"
"hint": "Przeczytamy tę witrynę, aby oprzeć kurs na Twoim prawdziwym produkcie."
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "Zostań ekspertem w dziedzinie X", "title": "Zostań ekspertem w dziedzinie X",
@@ -2419,7 +2417,12 @@
"error_description": "Ta domena niestandardowa istnieje, ale weryfikacja nie została jeszcze ukończona.", "error_description": "Ta domena niestandardowa istnieje, ale weryfikacja nie została jeszcze ukończona.",
"page_subtitle": "Run your public learning site on a web address you own.", "page_subtitle": "Run your public learning site on a web address you own.",
"dns_record": "Rekord DNS", "dns_record": "Rekord DNS",
"verified_snackbar": "Zweryfikowano domenę niestandardową" "verified_snackbar": "Zweryfikowano domenę niestandardową",
"record_active": "Wykryto",
"record_pending": "Oczekujące",
"ssl_pending_heading": "Certyfikat SSL jest wydawany",
"ssl_pending_body": "Własność Twojej domeny została zweryfikowana. Czekamy teraz, aż Cloudflare wystawi i wdroży certyfikat SSL. Zwykle zajmuje to kilka minut, gdy wszystkie poniższe rekordy DNS są już na swoim miejscu.",
"validation_errors_heading": "Wystawienie certyfikatu jest zablokowane"
} }
}, },
"quiz": { "quiz": {
@@ -2164,9 +2164,7 @@
"depth": "Quão profundo o curso deve ir?", "depth": "Quão profundo o curso deve ir?",
"topic": "Tópico ou nicho", "topic": "Tópico ou nicho",
"expertise_level": "Nível de especialização alvo", "expertise_level": "Nível de especialização alvo",
"documentation_url": { "documentation_url": "URL da documentação (opcional)"
"hint": "Leremos este site para fundamentar o curso em seu produto real."
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "Torne-se um especialista em X", "title": "Torne-se um especialista em X",
@@ -2419,7 +2417,12 @@
"error_description": "Este domínio personalizado existe, mas a verificação ainda não foi concluída.", "error_description": "Este domínio personalizado existe, mas a verificação ainda não foi concluída.",
"page_subtitle": "Run your public learning site on a web address you own.", "page_subtitle": "Run your public learning site on a web address you own.",
"dns_record": "Registro DNS", "dns_record": "Registro DNS",
"verified_snackbar": "Domínio personalizado verificado" "verified_snackbar": "Domínio personalizado verificado",
"record_active": "Detectado",
"record_pending": "Pendente",
"ssl_pending_heading": "O certificado SSL está sendo emitido",
"ssl_pending_body": "A propriedade do seu domínio é verificada. Agora estamos aguardando a Cloudflare emitir e implantar o certificado SSL. Isso geralmente leva alguns minutos depois que todos os registros DNS abaixo estiverem em vigor.",
"validation_errors_heading": "A emissão do certificado está bloqueada"
} }
}, },
"quiz": { "quiz": {
@@ -2164,9 +2164,7 @@
"depth": "Насколько глубоким должен быть курс?", "depth": "Насколько глубоким должен быть курс?",
"topic": "Тема или ниша", "topic": "Тема или ниша",
"expertise_level": "Целевой уровень экспертизы", "expertise_level": "Целевой уровень экспертизы",
"documentation_url": { "documentation_url": "URL-адрес документации (необязательно)"
"hint": "Мы прочитаем этот сайт, чтобы закрепить курс на вашем реальном продукте."
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "Станьте экспертом по X", "title": "Станьте экспертом по X",
@@ -2419,7 +2417,12 @@
"error_description": "Этот личный домен существует, но проверка еще не завершена.", "error_description": "Этот личный домен существует, но проверка еще не завершена.",
"page_subtitle": "Run your public learning site on a web address you own.", "page_subtitle": "Run your public learning site on a web address you own.",
"dns_record": "DNS-запись", "dns_record": "DNS-запись",
"verified_snackbar": "Персональный домен подтвержден" "verified_snackbar": "Персональный домен подтвержден",
"record_active": "Обнаружено",
"record_pending": "Ожидается",
"ssl_pending_heading": "SSL-сертификат выдается",
"ssl_pending_body": "Право собственности на ваш домен подтверждено. Теперь мы ждем, пока Cloudflare выпустит и развернет сертификат SSL. Обычно это занимает несколько минут после того, как все указанные ниже записи DNS будут на месте.",
"validation_errors_heading": "Выдача сертификата заблокирована"
} }
}, },
"quiz": { "quiz": {
@@ -2164,9 +2164,7 @@
"depth": "Khóa học nên đi sâu đến mức nào?", "depth": "Khóa học nên đi sâu đến mức nào?",
"topic": "Chủ đề hoặc niche", "topic": "Chủ đề hoặc niche",
"expertise_level": "Mục tiêu trình độ chuyên môn", "expertise_level": "Mục tiêu trình độ chuyên môn",
"documentation_url": { "documentation_url": "URL tài liệu (tùy chọn)"
"hint": "Chúng tôi sẽ đọc trang web này để xây dựng khóa học về sản phẩm thực của bạn."
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "Trở thành chuyên gia về X", "title": "Trở thành chuyên gia về X",
@@ -2419,7 +2417,12 @@
"error_description": "Miền tùy chỉnh này tồn tại nhưng quá trình xác minh chưa hoàn tất.", "error_description": "Miền tùy chỉnh này tồn tại nhưng quá trình xác minh chưa hoàn tất.",
"page_subtitle": "Run your public learning site on a web address you own.", "page_subtitle": "Run your public learning site on a web address you own.",
"dns_record": "bản ghi DNS", "dns_record": "bản ghi DNS",
"verified_snackbar": "Đã xác minh miền tùy chỉnh" "verified_snackbar": "Đã xác minh miền tùy chỉnh",
"record_active": "Đã phát hiện",
"record_pending": "Đang chờ xử lý",
"ssl_pending_heading": "Chứng chỉ SSL đang được cấp",
"ssl_pending_body": "Quyền sở hữu tên miền của bạn đã được xác minh. Hiện chúng tôi đang chờ Cloudflare phát hành và triển khai chứng chỉ SSL. Quá trình này thường mất vài phút sau khi tất cả bản ghi DNS bên dưới đã sẵn sàng.",
"validation_errors_heading": "Việc cấp chứng chỉ bị chặn"
} }
}, },
"quiz": { "quiz": {
@@ -7,7 +7,7 @@
let { data } = $props(); let { data } = $props();
</script> </script>
<Page.Root class="w-full"> <Page.Root class="max-auto w-[90%] px-4">
<Page.Header> <Page.Header>
<Page.HeaderContent> <Page.HeaderContent>
<Page.Title> <Page.Title>