feat: support custom domain (#462)
* feat: add custom domain CRUD * chore: fetch organization in layout in server before render * debugging * update log
This commit is contained in:
@@ -1,30 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { Grid, Row, Column } from 'carbon-components-svelte';
|
||||
import isValidDomain from 'is-valid-domain';
|
||||
import { parse } from 'tldts';
|
||||
import { Grid, Row, Column, CopyButton } from 'carbon-components-svelte';
|
||||
import Restart from 'carbon-icons-svelte/lib/Restart.svelte';
|
||||
import TrashCan from 'carbon-icons-svelte/lib/TrashCan.svelte';
|
||||
import ArrowUpRight from 'carbon-icons-svelte/lib/ArrowUpRight.svelte';
|
||||
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import type { CurrentOrg } from '$lib/utils/types/org';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { blockedSubdomain } from '$lib/utils/constants/app';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
|
||||
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import { updateOrgSiteNameValidation } from '$lib/utils/functions/validator';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { sendDomainRequest } from '$lib/utils/services/org/domain';
|
||||
|
||||
let siteName = '';
|
||||
let customDomain = '';
|
||||
let customCode = '';
|
||||
let favicon = '';
|
||||
let isDomainValid = false;
|
||||
let isLoading = false;
|
||||
let isCustomDomainLoading = false;
|
||||
let isRefreshing = false;
|
||||
|
||||
type Error = {
|
||||
siteName: string;
|
||||
customDomain: string;
|
||||
};
|
||||
|
||||
let siteName = '';
|
||||
let errors: Error = {
|
||||
siteName: ''
|
||||
siteName: '',
|
||||
customDomain: ''
|
||||
};
|
||||
let isLoading = false;
|
||||
|
||||
async function handleChangeDomain() {
|
||||
async function handleSaveSiteName() {
|
||||
errors = updateOrgSiteNameValidation(siteName) as Error;
|
||||
|
||||
if (Object.values(errors).length) {
|
||||
@@ -57,20 +78,129 @@
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
function setSiteName(_siteName: string) {
|
||||
if (!siteName) {
|
||||
siteName = _siteName;
|
||||
async function handleSaveCustomDomain() {
|
||||
if (!isDomainValid) return;
|
||||
|
||||
const details = parse(customDomain);
|
||||
if (!details.subdomain) {
|
||||
errors.customDomain = $t('components.settings.domains.custom_domain_error');
|
||||
return;
|
||||
}
|
||||
isCustomDomainLoading = true;
|
||||
|
||||
const { error } = await supabase
|
||||
.from('organization')
|
||||
.update({ customDomain })
|
||||
.match({ id: $currentOrg.id });
|
||||
|
||||
if (error) {
|
||||
console.log('Error: create organisation', error);
|
||||
errors.customDomain = error.message;
|
||||
isCustomDomainLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await sendDomainRequest('add_domain', customDomain);
|
||||
const data = await response.json();
|
||||
console.log('added domain to vercel', data);
|
||||
} catch (error) {
|
||||
console.log('Error: adding domain to vercel', error);
|
||||
snackbar.error(error as string);
|
||||
isCustomDomainLoading = false;
|
||||
return;
|
||||
}
|
||||
snackbar.success('components.settings.domains.custom_domain_success');
|
||||
$currentOrg.customDomain = customDomain;
|
||||
|
||||
isCustomDomainLoading = false;
|
||||
}
|
||||
|
||||
function resetErrors(_siteName: string) {
|
||||
async function handleRemoveCustomDomain() {
|
||||
if (!$currentOrg.customDomain) return;
|
||||
|
||||
isCustomDomainLoading = true;
|
||||
const { error } = await supabase
|
||||
.from('organization')
|
||||
.update({ customDomain: null, isCustomDomainVerified: false })
|
||||
.match({ id: $currentOrg.id });
|
||||
|
||||
if (error) {
|
||||
console.log('Error: updating organisation', error);
|
||||
snackbar.error(error.message);
|
||||
isCustomDomainLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await sendDomainRequest('remove_domain', $currentOrg.customDomain);
|
||||
const data = await response.json();
|
||||
if (!data.success) {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
console.log('removed domain from vercel', data);
|
||||
} catch (error) {
|
||||
console.log('Error: removing domain from vercel', error);
|
||||
snackbar.error(error as string);
|
||||
isCustomDomainLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
snackbar.success();
|
||||
|
||||
$currentOrg.customDomain = undefined;
|
||||
$currentOrg.isCustomDomainVerified = false;
|
||||
isCustomDomainLoading = false;
|
||||
}
|
||||
|
||||
async function handleRefreshCustomDomain() {
|
||||
isRefreshing = true;
|
||||
|
||||
try {
|
||||
const response = await sendDomainRequest('verify_domain', $currentOrg.customDomain || '');
|
||||
const data = await response.json();
|
||||
|
||||
console.log('data', data);
|
||||
if (data.verified && !$currentOrg.isCustomDomainVerified) {
|
||||
$currentOrg.isCustomDomainVerified = true;
|
||||
|
||||
await supabase
|
||||
.from('organization')
|
||||
.update({ isCustomDomainVerified: true })
|
||||
.match({ id: $currentOrg.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error: refreshing domain', error);
|
||||
snackbar.error(error as string);
|
||||
}
|
||||
|
||||
isRefreshing = false;
|
||||
}
|
||||
|
||||
function resetErrors(_siteName: string, _customDomain: string) {
|
||||
if (errors.siteName) {
|
||||
errors.siteName = '';
|
||||
}
|
||||
if (errors.customDomain) {
|
||||
errors.customDomain = '';
|
||||
}
|
||||
}
|
||||
|
||||
$: resetErrors(siteName);
|
||||
$: setSiteName($currentOrg.siteName);
|
||||
function setDefaults(org: CurrentOrg) {
|
||||
if (!siteName) {
|
||||
siteName = org.siteName;
|
||||
}
|
||||
}
|
||||
|
||||
function getSubdomain() {
|
||||
const details = parse($currentOrg.customDomain || '');
|
||||
return details.subdomain;
|
||||
}
|
||||
|
||||
$: setDefaults($currentOrg);
|
||||
$: resetErrors(siteName, customDomain);
|
||||
|
||||
$: isDomainValid = isValidDomain(customDomain, { subdomain: true });
|
||||
</script>
|
||||
|
||||
<Grid class="border rounded border-gray-200 dark:border-neutral-600 w-full mt-5">
|
||||
@@ -80,7 +210,7 @@
|
||||
>
|
||||
<Column sm={2} md={6} lg={8}>
|
||||
<p class="text-md text-gray-500 dark:text-white mb-5">
|
||||
{$t('components.settings.domains.work_together')}
|
||||
{$t('settings.organization.organization_profile.custom_domain.body')}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
@@ -100,7 +230,7 @@
|
||||
label={$t('components.settings.domains.update')}
|
||||
className="py-4"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={handleChangeDomain}
|
||||
onClick={handleSaveSiteName}
|
||||
isDisabled={isLoading}
|
||||
/>
|
||||
|
||||
@@ -110,15 +240,168 @@
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="py-7 border-bottom-c">
|
||||
<Column sm={2} md={2} lg={4} class="text-lg"
|
||||
><SectionTitle>{$t('components.settings.domains.custom')}</SectionTitle></Column
|
||||
>
|
||||
<Column sm={2} md={6} lg={8}>
|
||||
<div class="flex items-center">
|
||||
<p class="dark:text-white mr-3">{$t('components.settings.domains.domain')}</p>
|
||||
<ComingSoon />
|
||||
<Row class="py-7">
|
||||
<UpgradeBanner className="mb-5">{$t('upgrade.domain')}</UpgradeBanner>
|
||||
<Column sm={2} md={2} lg={4} class="text-lg">
|
||||
<SectionTitle>
|
||||
{$t('components.settings.domains.custom')}
|
||||
</SectionTitle>
|
||||
</Column>
|
||||
<Column sm={2} md={6} lg={10}>
|
||||
<div class="pb-10 border-bottom-c">
|
||||
<!-- DNS Configuration -->
|
||||
{#if $currentOrg.customDomain}
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="font-medium text-md flex items-center gap-2">
|
||||
{$currentOrg.customDomain}
|
||||
|
||||
<IconButton
|
||||
contained={true}
|
||||
size="small"
|
||||
onClick={() => goto(`https://${$currentOrg.customDomain}`)}
|
||||
>
|
||||
<ArrowUpRight size={16} />
|
||||
</IconButton>
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="mt-1 h-2 w-2 rounded-full bg-{$currentOrg.isCustomDomainVerified
|
||||
? 'green'
|
||||
: 'yellow'}-400"
|
||||
></div>
|
||||
</div>
|
||||
{#if $currentOrg.isCustomDomainVerified}
|
||||
<TextChip
|
||||
value="Verified"
|
||||
className="bg-green-500 text-white text-xs px-3"
|
||||
size="sm"
|
||||
/>
|
||||
{:else}
|
||||
<TextChip
|
||||
value="Pending verification"
|
||||
className="bg-yellow-500 text-white text-xs px-3"
|
||||
size="sm"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-gray-500 mb-4">
|
||||
{$t('components.settings.domains.dns_description')}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-10 mb-4 border rounded-md py-2 px-4">
|
||||
<div class="h-[72px] justify-evenly flex flex-col">
|
||||
<p class="font-light text-sm">{$t('components.settings.domains.dns_type')}</p>
|
||||
<p class="h-[40px] flex items-center">CNAME</p>
|
||||
</div>
|
||||
|
||||
<div class="h-[72px] justify-evenly flex flex-col">
|
||||
<p class="font-light text-sm">{$t('components.settings.domains.dns_name')}</p>
|
||||
<p class="h-[40px] flex items-center">
|
||||
{getSubdomain()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="h-[72px] justify-evenly flex flex-col">
|
||||
<p class="font-light text-sm">{$t('components.settings.domains.dns_value')}</p>
|
||||
<p class=" flex items-center gap-1">
|
||||
cname.vercel-dns.com
|
||||
<CopyButton text="cname.vercel-dns.com" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-5">
|
||||
<PrimaryButton
|
||||
className="py-4 flex items-center gap-2"
|
||||
onClick={handleRefreshCustomDomain}
|
||||
isLoading={isRefreshing}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
>
|
||||
{#if !isRefreshing}
|
||||
<Restart size={16} />
|
||||
{/if}
|
||||
{$t('components.settings.domains.refresh')}
|
||||
</PrimaryButton>
|
||||
|
||||
<PrimaryButton
|
||||
className="py-4 flex items-center gap-2"
|
||||
onClick={handleRemoveCustomDomain}
|
||||
isLoading={isCustomDomainLoading}
|
||||
variant={VARIANTS.CONTAINED_DANGER}
|
||||
>
|
||||
{#if !isCustomDomainLoading}
|
||||
<TrashCan size={16} />
|
||||
{/if}
|
||||
{$t('components.settings.domains.remove')}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Add Custom Domain -->
|
||||
<div class="flex items-center gap-5 mb-4">
|
||||
<p class="font-bold">{$t('components.settings.domains.your_domain')}</p>
|
||||
</div>
|
||||
<TextField
|
||||
bind:value={customDomain}
|
||||
type="text"
|
||||
placeholder="courses.yourwebsite.com"
|
||||
helperMessage="https://{customDomain || 'course.yourwebsite.com'}"
|
||||
errorMessage={errors.customDomain}
|
||||
className="w-4/5"
|
||||
/>
|
||||
|
||||
<div class="flex items-center mt-5">
|
||||
<PrimaryButton
|
||||
label={$t('components.settings.domains.save')}
|
||||
className="py-4"
|
||||
onClick={handleSaveCustomDomain}
|
||||
isLoading={isCustomDomainLoading}
|
||||
isDisabled={isLoading || !isDomainValid}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="py-10 border-bottom-c">
|
||||
<div class="flex items-center gap-5 mb-4">
|
||||
<p class="font-bold">{$t('components.settings.domains.custom_favicon')}</p>
|
||||
<ComingSoon />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<UploadImage
|
||||
shape="rounded-md"
|
||||
bind:avatar={favicon}
|
||||
src={$currentOrg.favicon ?? '/logo-512.png'}
|
||||
widthHeight="w-16 h-16 lg:w-24 lg:h-24"
|
||||
flexDirection="flex-row"
|
||||
isDisabled={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-10">
|
||||
<div class="flex items-center gap-5 mb-4">
|
||||
<p class="font-bold">{$t('components.settings.domains.custom_code')}</p>
|
||||
<ComingSoon />
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
bind:value={customCode}
|
||||
placeholder="e.g <link rel='stylesheet' href='https://example.com/style.css' />"
|
||||
className="w-4/5"
|
||||
rows={7}
|
||||
disabled={true}
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="flex items-center">
|
||||
<p class="dark:text-white mr-3">
|
||||
{$t('components.settings.domains.domain')}
|
||||
</p>
|
||||
|
||||
<ComingSoon />
|
||||
</div> -->
|
||||
</Column>
|
||||
</Row>
|
||||
</Grid>
|
||||
|
||||
@@ -234,11 +234,15 @@
|
||||
{$t('settings.organization.organization_profile.custom_domain.body')}
|
||||
</p>
|
||||
<PrimaryButton
|
||||
label={$t('settings.organization.organization_profile.custom_domain.button')}
|
||||
className="my-7 py-5 px-10"
|
||||
className="my-7 py-5 px-10 flex items-center gap-2 justify-center"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => gotoSetting('/domains')}
|
||||
/>
|
||||
>
|
||||
{#if $isFreePlan}
|
||||
<FlashFilled size={16} class="text-blue-700" />
|
||||
{/if}
|
||||
{$t('settings.organization.organization_profile.custom_domain.button')}
|
||||
</PrimaryButton>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||
|
||||
Reference in New Issue
Block a user