diff --git a/apps/dashboard/.env.example b/apps/dashboard/.env.example
index ca967be8..fa226070 100644
--- a/apps/dashboard/.env.example
+++ b/apps/dashboard/.env.example
@@ -30,4 +30,9 @@ PRIVATE_APP_SUBDOMAINS=app
LEMON_SQUEEZY_API_KEY=
LEMON_SQUEEZY_STORE_ID=
-LEMON_SQUEEZY_WEBHOOK_SECRET=
\ No newline at end of file
+LEMON_SQUEEZY_WEBHOOK_SECRET=
+
+## Custom Domain
+TEAM_ID_VERCEL=
+PROJECT_ID_VERCEL=
+AUTH_BEARER_TOKEN=
diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json
index 17d0c958..434279df 100644
--- a/apps/dashboard/package.json
+++ b/apps/dashboard/package.json
@@ -46,6 +46,7 @@
"form-data": "^4.0.0",
"hotkeys-js": "^3.11.2",
"html-to-image": "^1.11.11",
+ "is-valid-domain": "^0.1.6",
"js-yaml": "^4.1.0",
"jspdf": "^2.5.1",
"jspdf-autotable": "^3.8.2",
@@ -61,6 +62,7 @@
"svelte-meta-tags": "^3.1.2",
"svelte-ui": "workspace:*",
"sveltekit-i18n": "^2.4.2",
+ "tldts": "^6.1.41",
"unsplash-js": "^7.0.18",
"wait-on": "^7.0.1",
"zod": "^3.21.4"
diff --git a/apps/dashboard/src/app.postcss b/apps/dashboard/src/app.postcss
index 2df30a11..ce2f08d0 100644
--- a/apps/dashboard/src/app.postcss
+++ b/apps/dashboard/src/app.postcss
@@ -141,6 +141,29 @@ code {
overflow-y: auto;
}
+.shake {
+ animation: shake 0.85s;
+}
+@keyframes shake {
+ 10%,
+ 90% {
+ transform: translate3d(-15px, 0, 0);
+ }
+ 20%,
+ 80% {
+ transform: translate3d(15px, 0, 0);
+ }
+ 30%,
+ 50%,
+ 70% {
+ transform: translate3d(-15px, 0, 0);
+ }
+ 40%,
+ 60% {
+ transform: translate3d(15px, 0, 0);
+ }
+}
+
.bx--list-box__menu-item__option {
white-space: normal !important;
}
diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/ViewMode.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/ViewMode.svelte
index 2e009185..a8565329 100644
--- a/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/ViewMode.svelte
+++ b/apps/dashboard/src/lib/components/Course/components/Lesson/Exercise/ViewMode.svelte
@@ -376,26 +376,4 @@
.status-text {
width: fit-content;
}
- :global(.shake) {
- animation: shake 0.85s;
- }
- @keyframes shake {
- 10%,
- 90% {
- transform: translate3d(-15px, 0, 0);
- }
- 20%,
- 80% {
- transform: translate3d(15px, 0, 0);
- }
- 30%,
- 50%,
- 70% {
- transform: translate3d(-15px, 0, 0);
- }
- 40%,
- 60% {
- transform: translate3d(15px, 0, 0);
- }
- }
diff --git a/apps/dashboard/src/lib/components/Form/TextField.svelte b/apps/dashboard/src/lib/components/Form/TextField.svelte
index f4ff7b4d..67805633 100644
--- a/apps/dashboard/src/lib/components/Form/TextField.svelte
+++ b/apps/dashboard/src/lib/components/Form/TextField.svelte
@@ -102,7 +102,7 @@
{#if errorMessage}
{errorMessage}
{:else if helperMessage}
-
+
{helperMessage}
{/if}
diff --git a/apps/dashboard/src/lib/components/Org/Settings/Domains.svelte b/apps/dashboard/src/lib/components/Org/Settings/Domains.svelte
index d317fed4..5ed07b4f 100644
--- a/apps/dashboard/src/lib/components/Org/Settings/Domains.svelte
+++ b/apps/dashboard/src/lib/components/Org/Settings/Domains.svelte
@@ -1,30 +1,51 @@
@@ -80,7 +210,7 @@
>
- {$t('components.settings.domains.work_together')}
+ {$t('settings.organization.organization_profile.custom_domain.body')}
@@ -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 @@
-
- {$t('components.settings.domains.custom')}
-
-
-
{$t('components.settings.domains.domain')}
-
+
+ {$t('upgrade.domain')}
+
+
+ {$t('components.settings.domains.custom')}
+
+
+
+
+
+ {#if $currentOrg.customDomain}
+
+
+
+ {$currentOrg.customDomain}
+
+ goto(`https://${$currentOrg.customDomain}`)}
+ >
+
+
+
+
+
+
+ {#if $currentOrg.isCustomDomainVerified}
+
+ {:else}
+
+ {/if}
+
+
+
+ {$t('components.settings.domains.dns_description')}
+
+
+
+
+
{$t('components.settings.domains.dns_type')}
+
CNAME
+
+
+
+
{$t('components.settings.domains.dns_name')}
+
+ {getSubdomain()}
+
+
+
+
+
{$t('components.settings.domains.dns_value')}
+
+ cname.vercel-dns.com
+
+
+
+
+
+
+
+ {#if !isRefreshing}
+
+ {/if}
+ {$t('components.settings.domains.refresh')}
+
+
+
+ {#if !isCustomDomainLoading}
+
+ {/if}
+ {$t('components.settings.domains.remove')}
+
+
+ {:else}
+
+
+
{$t('components.settings.domains.your_domain')}
+
+
+
+
+ {/if}
+
+
+
+
{$t('components.settings.domains.custom_favicon')}
+
+
+
+
+
+
+
+
+
+
+
{$t('components.settings.domains.custom_code')}
+
+
+
+
+
+
diff --git a/apps/dashboard/src/lib/components/Org/Settings/OrgSettings.svelte b/apps/dashboard/src/lib/components/Org/Settings/OrgSettings.svelte
index 05628ea4..8dd3de75 100644
--- a/apps/dashboard/src/lib/components/Org/Settings/OrgSettings.svelte
+++ b/apps/dashboard/src/lib/components/Org/Settings/OrgSettings.svelte
@@ -234,11 +234,15 @@
{$t('settings.organization.organization_profile.custom_domain.body')}
gotoSetting('/domains')}
- />
+ >
+ {#if $isFreePlan}
+
+ {/if}
+ {$t('settings.organization.organization_profile.custom_domain.button')}
+
diff --git a/apps/dashboard/src/lib/components/Upgrade/Modal.svelte b/apps/dashboard/src/lib/components/Upgrade/Modal.svelte
index b4206b73..79d9c4e0 100644
--- a/apps/dashboard/src/lib/components/Upgrade/Modal.svelte
+++ b/apps/dashboard/src/lib/components/Upgrade/Modal.svelte
@@ -157,21 +157,23 @@
style="background-color: {isYearlyPlan ? 'initial' : '#1D4EE2'}; color: {isYearlyPlan
? '#5e636b'
: '#fff'}"
- class="rounded-[30px] bg-blue-700 px-3 py-1 text-xs text-white lg:px-4 lg:py-2"
- on:click={toggleIsYearlyPlan}>{$t('pricing.modal.monthly')}
+ {$t('pricing.modal.monthly')}
+
diff --git a/apps/dashboard/src/lib/components/UploadImage/index.svelte b/apps/dashboard/src/lib/components/UploadImage/index.svelte
index 82bfb23d..b62849b9 100644
--- a/apps/dashboard/src/lib/components/UploadImage/index.svelte
+++ b/apps/dashboard/src/lib/components/UploadImage/index.svelte
@@ -7,22 +7,33 @@
export let widthHeight = '';
export let shape = 'rounded-full';
export let errorMessage: string | null = null;
+ export let isDisabled = false;
+ export let maxFileSizeInMb: number = 2; // Default max file size 2MB
+ export let flexDirection = 'flex-col';
let fileinput: HTMLInputElement;
const onFileSelected = (e: Event) => {
const image = e.target.files[0];
+ const maxFileSize = maxFileSizeInMb * 1024 * 1024;
+ if (image.size > maxFileSize) {
+ errorMessage = `${$t('settings.profile.profile_picture.validation_error')} ${
+ maxFileSize / (1024 * 1024)
+ } MB`;
+ return;
+ }
let reader = new FileReader();
reader.readAsDataURL(image);
reader.onload = (e) => {
avatar = image;
// @ts-ignore
src = e.target?.result || undefined;
+ errorMessage = null; // Clear error message on successful load
};
};
-
+
-
- {#if errorMessage}
-
{errorMessage}
- {/if}
+
+
+
+ {$t('settings.profile.profile_picture.file_size')}
+ {maxFileSizeInMb}MB
+ {$t('settings.profile.profile_picture.accepted')}: jpeg, jpg, png, webp
+
+ {#if errorMessage}
+
{errorMessage}
+ {/if}
+
{
+ return await fetch(
+ `https://api.vercel.com/v10/projects/${PROJECT_ID_VERCEL}/domains${
+ TEAM_ID_VERCEL ? `?teamId=${TEAM_ID_VERCEL}` : ''
+ }`,
+ {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${AUTH_BEARER_TOKEN}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ name: domain
+ // Optional: Redirect www. to root domain
+ // ...(domain.startsWith("www.") && {
+ // redirect: domain.replace("www.", ""),
+ // }),
+ })
+ }
+ ).then((res) => res.json());
+};
+
+export const removeDomainFromVercelProject = async (domain: string) => {
+ return await fetch(
+ `https://api.vercel.com/v9/projects/${PROJECT_ID_VERCEL}/domains/${domain}${
+ TEAM_ID_VERCEL ? `?teamId=${TEAM_ID_VERCEL}` : ''
+ }`,
+ {
+ headers: {
+ Authorization: `Bearer ${AUTH_BEARER_TOKEN}`
+ },
+ method: 'DELETE'
+ }
+ ).then((res) => res.json());
+};
+
+export const removeDomainFromVercelTeam = async (domain: string) => {
+ return await fetch(
+ `https://api.vercel.com/v6/domains/${domain}${
+ TEAM_ID_VERCEL ? `?teamId=${TEAM_ID_VERCEL}` : ''
+ }`,
+ {
+ headers: {
+ Authorization: `Bearer ${AUTH_BEARER_TOKEN}`
+ },
+ method: 'DELETE'
+ }
+ ).then((res) => res.json());
+};
+
+export const getDomainResponse = async (
+ domain: string
+): Promise
=> {
+ return await fetch(
+ `https://api.vercel.com/v9/projects/${PROJECT_ID_VERCEL}/domains/${domain}${
+ TEAM_ID_VERCEL ? `?teamId=${TEAM_ID_VERCEL}` : ''
+ }`,
+ {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${AUTH_BEARER_TOKEN}`,
+ 'Content-Type': 'application/json'
+ }
+ }
+ ).then((res) => {
+ return res.json();
+ });
+};
+
+export const getConfigResponse = async (domain: string): Promise => {
+ return await fetch(
+ `https://api.vercel.com/v6/domains/${domain}/config${
+ TEAM_ID_VERCEL ? `?teamId=${TEAM_ID_VERCEL}` : ''
+ }`,
+ {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${AUTH_BEARER_TOKEN}`,
+ 'Content-Type': 'application/json'
+ }
+ }
+ ).then((res) => res.json());
+};
+
+export const verifyDomain = async (domain: string): Promise => {
+ return await fetch(
+ `https://api.vercel.com/v9/projects/${PROJECT_ID_VERCEL}/domains/${domain}/verify${
+ TEAM_ID_VERCEL ? `?teamId=${TEAM_ID_VERCEL}` : ''
+ }`,
+ {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${AUTH_BEARER_TOKEN}`,
+ 'Content-Type': 'application/json'
+ }
+ }
+ ).then((res) => res.json());
+};
+
+export const getSubdomain = (name: string, apexName: string) => {
+ if (name === apexName) return null;
+ return name.slice(0, name.length - apexName.length - 1);
+};
+
+export const getApexDomain = (url: string) => {
+ let domain;
+ try {
+ domain = new URL(url).hostname;
+ } catch (e) {
+ return '';
+ }
+ const parts = domain.split('.');
+ if (parts.length > 2) {
+ // if it's a subdomain (e.g. project.vercel.app), return the last 2 parts
+ return parts.slice(-2).join('.');
+ }
+ // if it's a normal domain (e.g. training.yourdomain.com), we return the domain
+ return domain;
+};
+
+// courtesy of ChatGPT: https://sharegpt.com/c/pUYXtRs
+export const validDomainRegex = new RegExp(
+ /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/
+);
diff --git a/apps/dashboard/src/lib/utils/services/org/domain.ts b/apps/dashboard/src/lib/utils/services/org/domain.ts
new file mode 100644
index 00000000..9faf903e
--- /dev/null
+++ b/apps/dashboard/src/lib/utils/services/org/domain.ts
@@ -0,0 +1,15 @@
+import { supabase } from '$lib/utils/functions/supabase';
+
+export async function sendDomainRequest(key: string, domain: string): Promise {
+ const { data } = await supabase.auth.getSession();
+ const accessToken = data.session?.access_token || '';
+
+ return fetch('/api/domain', {
+ method: 'POST',
+ body: JSON.stringify({ params: { key, domain } }),
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `${accessToken}`
+ }
+ });
+}
diff --git a/apps/dashboard/src/lib/utils/services/org/index.ts b/apps/dashboard/src/lib/utils/services/org/index.ts
index 8b40d671..296e1345 100644
--- a/apps/dashboard/src/lib/utils/services/org/index.ts
+++ b/apps/dashboard/src/lib/utils/services/org/index.ts
@@ -73,15 +73,7 @@ export async function getOrganizations(userId: string, isOrgSite?: boolean, orgS
role_id,
created_at,
organization!organizationmember_organization_id_fkey (
- id,
- name,
- siteName,
- avatar_url,
- landingpage,
- customization,
- is_restricted,
- theme,
- created_at,
+ *,
organization_plan(
plan_name,
is_active,
@@ -107,18 +99,10 @@ export async function getOrganizations(userId: string, isOrgSite?: boolean, orgS
if (Array.isArray(data) && data.length) {
data.forEach((orgMember) => {
orgsArray.push({
- id: orgMember?.organization?.id,
- name: orgMember?.organization?.name,
- shortName: orgMember?.organization?.name?.substring(0, 2)?.toUpperCase() || '',
- siteName: orgMember?.organization?.siteName,
- theme: orgMember?.organization?.theme,
- avatar_url: orgMember?.organization?.avatar_url,
+ ...(orgMember?.organization || {}),
memberId: orgMember?.id,
- role_id: orgMember?.role_id,
- landingpage: orgMember?.organization?.landingpage,
- is_restricted: orgMember?.organization?.is_restricted,
- customization: orgMember?.organization?.customization,
- organization_plan: orgMember?.organization?.organization_plan
+ role_id: parseInt(orgMember?.role_id),
+ shortName: orgMember?.organization?.name?.substring(0, 2)?.toUpperCase() || ''
});
});
@@ -220,7 +204,7 @@ export async function getCourseBySiteName(siteName: string) {
return data;
}
-export async function getCurrentOrg(siteName: string, justGet = false) {
+export async function getCurrentOrg(siteName: string, justGet = false, isCustomDomain = false) {
const { data, error } = await supabase
.from('organization')
.select(
@@ -233,13 +217,18 @@ export async function getCurrentOrg(siteName: string, justGet = false) {
is_restricted,
customization,
theme,
+ favicon,
+ customDomain,
+ isCustomDomainVerified,
+ customCode,
organization_plan(
plan_name,
is_active
)
`
)
- .eq('siteName', siteName)
+ .eq(isCustomDomain ? 'customDomain' : 'siteName', siteName)
+ .filter('isCustomDomainVerified', 'eq', isCustomDomain ? true : undefined)
.returns();
console.log('data =', data);
console.log('error =', error);
diff --git a/apps/dashboard/src/lib/utils/translations/de.json b/apps/dashboard/src/lib/utils/translations/de.json
index 88549c1e..4f8545c6 100644
--- a/apps/dashboard/src/lib/utils/translations/de.json
+++ b/apps/dashboard/src/lib/utils/translations/de.json
@@ -194,7 +194,8 @@
"team_members": "Teammitglieder",
"back": "Zurück zu den Einstellungen",
"download_lessons": "Führen Sie ein Upgrade durch, um Lektionen herunterzuladen",
- "download_course": "Upgrade zum Download-Kurs"
+ "download_course": "Upgrade zum Download-Kurs",
+ "domain": "Aktualisieren Sie Ihren Plan, um benutzerdefinierte Domänen hinzuzufügen"
},
"audience": {
"title": "Publikum",
@@ -923,12 +924,23 @@
"settings": {
"domains": {
"add": "Hinzufügen",
- "work_together": "Fügen Sie Ihre Teamkollegen oder Mitarbeiter zu Ihrer Organisation hinzu. Beginnen Sie mit der Zusammenarbeit",
"url": "URL",
"update": "Domäne aktualisieren",
"custom": "Benutzerdefinierten Domain",
"domain": "Fügen Sie Ihren eigenen Domainnamen hinzu",
- "title": "Benutzerdefinierten Domain"
+ "title": "Benutzerdefinierten Domain",
+ "save": "Speichern",
+ "your_domain": "Ihre Domain",
+ "custom_domain_error": "Sie können eine Top-Level-Domain nicht als benutzerdefinierte Domain hinzufügen. Verwenden Sie stattdessen eine Subdomain wie „Kurse.[IhreWebsite].com“ oder „www.[IhreWebsite].com“.",
+ "custom_domain_success": "Benutzerdefinierte Domäne erfolgreich hinzugefügt",
+ "dns_description": "Legen Sie bei Ihrem DNS-Anbieter den folgenden Eintrag fest, um fortzufahren:",
+ "dns_type": "Typ",
+ "dns_value": "Wert",
+ "dns_name": "Name",
+ "refresh": "Aktualisieren",
+ "remove": "Entfernen",
+ "custom_favicon": "Benutzerdefiniertes Favicon",
+ "custom_code": "Benutzerdefinierter Code"
},
"customize-lms": {
"title": "LMS anpassen",
@@ -1084,7 +1096,8 @@
"learn": "Erfahren Sie mehr",
"month": "Monat",
"monthly": "Monatlich",
- "annually": "Jährlich"
+ "annually": "Jährlich",
+ "save": "Sparen Sie 2 Monate"
},
"plan_names": {
"free": "Frei",
@@ -1206,9 +1219,9 @@
"update_organization": "Organisation aktualisieren",
"team": {
"heading": "Team",
- "sub_heading": "Richten Sie Ihre Website ein",
"body": "Fügen Sie Ihrer Organisation Teamkollegen hinzu, damit Sie beide an Projekten zusammenarbeiten können.",
- "button": "Team verwalten"
+ "button": "Team verwalten",
+ "sub_heading": "Stellen Sie Ihr Team zusammen"
},
"custom_domain": {
"heading": "Benutzerdefinierten Domain",
@@ -1239,7 +1252,10 @@
},
"profile_picture": {
"heading": "Profilbild",
- "upload_image": "Bild hochladen"
+ "upload_image": "Bild hochladen",
+ "file_size": "Maximale Dateigröße:",
+ "accepted": "Akzeptiert:",
+ "validation_error": "Die Dateigröße überschreitet die maximale Grenze:"
}
},
"tabs": {
diff --git a/apps/dashboard/src/lib/utils/translations/en.json b/apps/dashboard/src/lib/utils/translations/en.json
index fdfb259d..5e5cc5d7 100644
--- a/apps/dashboard/src/lib/utils/translations/en.json
+++ b/apps/dashboard/src/lib/utils/translations/en.json
@@ -66,7 +66,10 @@
"profile": {
"profile_picture": {
"heading": "Profile Picture",
- "upload_image": "Upload Image"
+ "upload_image": "Upload Image",
+ "file_size": "Max file size:",
+ "accepted": "Accepted:",
+ "validation_error": "File size exceeds the maximum limit:"
},
"personal_information": {
"heading": "Personal Information",
@@ -101,7 +104,7 @@
},
"team": {
"heading": "Team",
- "sub_heading": "Set up your website",
+ "sub_heading": "Set up your team",
"body": "Add team mates to your organization so you can both collaborate on projects.",
"button": "Manage Team"
}
@@ -224,6 +227,7 @@
"heading": "Upgrade Plan",
"thanks": "Thank you for your support",
"plan": "Your plan comes with cool perks and benefits including access to all future features we release.",
+ "save": "Save 2 months",
"close": "Close",
"learn": "Learn more",
"month": "month",
@@ -332,11 +336,22 @@
"domains": {
"title": "Custom domain",
"add": "Add",
- "work_together": "Add your team mates or collaborators to your organization. Start working together",
"url": "URL",
"update": "Update Domain",
"custom": "Custom domain",
- "domain": "Add your own domain name"
+ "domain": "Add your own domain name",
+ "save": "Save",
+ "your_domain": "Your Domain",
+ "custom_domain_error": "You can't add a top level domain as custom domain. Instead, use a subdomain like 'courses.[yourwebsite].com' or 'www.[yourwebsite].com'.",
+ "custom_domain_success": "Custom domain added successfully",
+ "dns_description": "Set the following record on your DNS provider to continue:",
+ "dns_type": "Type",
+ "dns_value": "Value",
+ "dns_name": "Name",
+ "refresh": "Refresh",
+ "remove": "Remove",
+ "custom_favicon": "Custom favicon",
+ "custom_code": "Custom code"
},
"customize_lms": {
"title": "Customize LMS",
@@ -1119,6 +1134,7 @@
"upgrade": {
"certificate": "Upgrade your plan to generate certificates",
"team": "Upgrade your plan to add team members",
+ "domain": "Upgrade your plan to add custom domains",
"team_members": "Team members",
"back": "Back to Settings",
"download_lessons": "Upgrade to download lessons",
diff --git a/apps/dashboard/src/lib/utils/translations/es.json b/apps/dashboard/src/lib/utils/translations/es.json
index 37533fed..eeff8f4a 100644
--- a/apps/dashboard/src/lib/utils/translations/es.json
+++ b/apps/dashboard/src/lib/utils/translations/es.json
@@ -194,7 +194,8 @@
"team_members": "Miembros del equipo",
"back": "Volver a configuración",
"download_lessons": "Actualice para descargar lecciones",
- "download_course": "Actualizar para descargar el curso"
+ "download_course": "Actualizar para descargar el curso",
+ "domain": "Actualice su plan para agregar dominios personalizados"
},
"audience": {
"title": "Audiencia",
@@ -923,12 +924,23 @@
"settings": {
"domains": {
"add": "Agregar",
- "work_together": "Agregue sus compañeros de equipo o colaboradores a su organización. Empezar a trabajar juntos",
"url": "URL",
"update": "Actualizar dominio",
"custom": "Dominio personalizado",
"domain": "Añade tu propio nombre de dominio",
- "title": "Dominio personalizado"
+ "title": "Dominio personalizado",
+ "save": "Ahorrar",
+ "your_domain": "Tu dominio",
+ "custom_domain_error": "No puede agregar un dominio de nivel superior como dominio personalizado. En su lugar, utilice un subdominio como 'cursos.[susitio web].com' o 'www.[susitio web].com'.",
+ "custom_domain_success": "Dominio personalizado agregado exitosamente",
+ "dns_description": "Establezca el siguiente registro en su proveedor de DNS para continuar:",
+ "dns_type": "Tipo",
+ "dns_value": "Valor",
+ "dns_name": "Nombre",
+ "refresh": "Refrescar",
+ "remove": "Eliminar",
+ "custom_favicon": "favicon personalizado",
+ "custom_code": "código personalizado"
},
"customize-lms": {
"title": "Personalizar LMS",
@@ -1084,7 +1096,8 @@
"learn": "Aprende más",
"month": "mes",
"monthly": "Mensual",
- "annually": "Anualmente"
+ "annually": "Anualmente",
+ "save": "Ahorra 2 meses"
},
"plan_names": {
"free": "Gratis",
@@ -1206,9 +1219,9 @@
"update_organization": "Actualizar organización",
"team": {
"heading": "Equipo",
- "sub_heading": "Configura tu sitio web",
"body": "Agregue compañeros de equipo a su organización para que ambos puedan colaborar en proyectos.",
- "button": "Gestionar equipo"
+ "button": "Gestionar equipo",
+ "sub_heading": "Configura tu equipo"
},
"custom_domain": {
"heading": "Dominio personalizado",
@@ -1239,7 +1252,10 @@
},
"profile_picture": {
"heading": "Foto de perfil",
- "upload_image": "Cargar imagen"
+ "upload_image": "Cargar imagen",
+ "file_size": "Tamaño máximo de archivo:",
+ "accepted": "Aceptado:",
+ "validation_error": "El tamaño del archivo excede el límite máximo:"
}
},
"tabs": {
diff --git a/apps/dashboard/src/lib/utils/translations/fr.json b/apps/dashboard/src/lib/utils/translations/fr.json
index 34621b44..d08fc7aa 100644
--- a/apps/dashboard/src/lib/utils/translations/fr.json
+++ b/apps/dashboard/src/lib/utils/translations/fr.json
@@ -194,7 +194,8 @@
"team_members": "Membres de l'équipe",
"back": "Retour aux paramètres",
"download_lessons": "Mettre à niveau pour télécharger les leçons",
- "download_course": "Mettre à niveau pour télécharger le cours"
+ "download_course": "Mettre à niveau pour télécharger le cours",
+ "domain": "Mettez à niveau votre forfait pour ajouter des domaines personnalisés"
},
"audience": {
"title": "Public",
@@ -923,12 +924,23 @@
"settings": {
"domains": {
"add": "Ajouter",
- "work_together": "Ajoutez vos coéquipiers ou collaborateurs à votre organisation. Commencez à travailler ensemble",
"url": "URL",
"update": "Mettre à jour le domaine",
"custom": "Domaine personnalisé",
"domain": "Ajoutez votre propre nom de domaine",
- "title": "Domaine personnalisé"
+ "title": "Domaine personnalisé",
+ "save": "Sauvegarder",
+ "your_domain": "Votre domaine",
+ "custom_domain_error": "Vous ne pouvez pas ajouter un domaine de premier niveau en tant que domaine personnalisé. Utilisez plutôt un sous-domaine tel que « courses.[votresite Web].com » ou « www.[votre site Web].com ».",
+ "custom_domain_success": "Domaine personnalisé ajouté avec succès",
+ "dns_description": "Définissez l'enregistrement suivant sur votre fournisseur DNS pour continuer :",
+ "dns_type": "Taper",
+ "dns_value": "Valeur",
+ "dns_name": "Nom",
+ "refresh": "Rafraîchir",
+ "remove": "Retirer",
+ "custom_favicon": "Icône de favori personnalisée",
+ "custom_code": "Code personnalisé"
},
"customize-lms": {
"title": "Personnaliser le LMS",
@@ -1084,7 +1096,8 @@
"learn": "Apprendre encore plus",
"month": "mois",
"monthly": "Mensuel",
- "annually": "Annuellement"
+ "annually": "Annuellement",
+ "save": "Économisez 2 mois"
},
"plan_names": {
"free": "Gratuit",
@@ -1206,9 +1219,9 @@
"update_organization": "Mettre à jour l'organisation",
"team": {
"heading": "Équipe",
- "sub_heading": "Configurez votre site Web",
"body": "Ajoutez des coéquipiers à votre organisation afin que vous puissiez tous les deux collaborer sur des projets.",
- "button": "Gérer l'équipe"
+ "button": "Gérer l'équipe",
+ "sub_heading": "Configurez votre équipe"
},
"custom_domain": {
"heading": "Domaine personnalisé",
@@ -1239,7 +1252,10 @@
},
"profile_picture": {
"heading": "Image de profil",
- "upload_image": "Télécharger une image"
+ "upload_image": "Télécharger une image",
+ "file_size": "Taille maximale du fichier :",
+ "accepted": "Accepté:",
+ "validation_error": "La taille du fichier dépasse la limite maximale :"
}
},
"tabs": {
diff --git a/apps/dashboard/src/lib/utils/translations/hi.json b/apps/dashboard/src/lib/utils/translations/hi.json
index ca0cae1c..ad41e339 100644
--- a/apps/dashboard/src/lib/utils/translations/hi.json
+++ b/apps/dashboard/src/lib/utils/translations/hi.json
@@ -194,7 +194,8 @@
"team_members": "टीम के सदस्य",
"back": "सेटिंग्स पर वापस जाएँ",
"download_lessons": "पाठ डाउनलोड करने के लिए अपग्रेड करें",
- "download_course": "पाठ्यक्रम डाउनलोड करने के लिए अपग्रेड करें"
+ "download_course": "पाठ्यक्रम डाउनलोड करने के लिए अपग्रेड करें",
+ "domain": "कस्टम डोमेन जोड़ने के लिए अपनी योजना को अपग्रेड करें"
},
"audience": {
"title": "श्रोता",
@@ -923,12 +924,23 @@
"settings": {
"domains": {
"add": "जोड़ना",
- "work_together": "अपने टीम के साथियों या सहयोगियों को अपने संगठन में जोड़ें। एक साथ काम करना शुरू करें",
"url": "यूआरएल",
"update": "डोमेन अद्यतन करें",
"custom": "कस्टम डोमेन",
"domain": "अपना स्वयं का डोमेन नाम जोड़ें",
- "title": "कस्टम डोमेन"
+ "title": "कस्टम डोमेन",
+ "save": "बचाना",
+ "your_domain": "आपका डोमेन",
+ "custom_domain_error": "आप किसी शीर्ष स्तरीय डोमेन को कस्टम डोमेन के रूप में नहीं जोड़ सकते. इसके बजाय, 'courses.[yourwebsite].com' या 'www.[yourwebsite].com' जैसे उपडोमेन का उपयोग करें।",
+ "custom_domain_success": "कस्टम डोमेन सफलतापूर्वक जोड़ा गया",
+ "dns_description": "जारी रखने के लिए अपने DNS प्रदाता पर निम्नलिखित रिकॉर्ड सेट करें:",
+ "dns_type": "प्रकार",
+ "dns_value": "कीमत",
+ "dns_name": "नाम",
+ "refresh": "ताज़ा करना",
+ "remove": "निकालना",
+ "custom_favicon": "कस्टम फ़ेविकॉन",
+ "custom_code": "कस्टम कोड"
},
"customize-lms": {
"title": "एलएमएस को अनुकूलित करें",
@@ -1084,7 +1096,8 @@
"learn": "और अधिक जानें",
"month": "महीना",
"monthly": "महीने के",
- "annually": "हर साल"
+ "annually": "हर साल",
+ "save": "2 महीने बचाएं"
},
"plan_names": {
"free": "मुक्त",
@@ -1206,9 +1219,9 @@
"update_organization": "संगठन अद्यतन करें",
"team": {
"heading": "टीम",
- "sub_heading": "अपनी वेबसाइट सेट करें",
"body": "अपने संगठन में टीम के साथियों को जोड़ें ताकि आप दोनों परियोजनाओं पर सहयोग कर सकें।",
- "button": "टीम का प्रबंधन करें"
+ "button": "टीम का प्रबंधन करें",
+ "sub_heading": "अपनी टीम स्थापित करें"
},
"custom_domain": {
"heading": "कस्टम डोमेन",
@@ -1239,7 +1252,10 @@
},
"profile_picture": {
"heading": "प्रोफ़ाइल फोटो",
- "upload_image": "तस्विर अपलोड करना"
+ "upload_image": "तस्विर अपलोड करना",
+ "file_size": "अधिकतम फ़ाइल आकार:",
+ "accepted": "स्वीकृत:",
+ "validation_error": "फ़ाइल का आकार अधिकतम सीमा से अधिक है:"
}
},
"tabs": {
diff --git a/apps/dashboard/src/lib/utils/translations/pt.json b/apps/dashboard/src/lib/utils/translations/pt.json
index a4f5f4d2..169d8eb0 100644
--- a/apps/dashboard/src/lib/utils/translations/pt.json
+++ b/apps/dashboard/src/lib/utils/translations/pt.json
@@ -194,7 +194,8 @@
"team_members": "Membros do time",
"back": "Voltar para configurações",
"download_lessons": "Atualize para baixar lições",
- "download_course": "Atualize para baixar o curso"
+ "download_course": "Atualize para baixar o curso",
+ "domain": "Atualize seu plano para adicionar domínios personalizados"
},
"audience": {
"title": "Público",
@@ -923,12 +924,23 @@
"settings": {
"domains": {
"add": "Adicionar",
- "work_together": "Adicione seus companheiros de equipe ou colaboradores à sua organização. Comece a trabalhar juntos",
"url": "URL",
"update": "Atualizar domínio",
"custom": "Domínio personalizado",
"domain": "Adicione seu próprio nome de domínio",
- "title": "Domínio personalizado"
+ "title": "Domínio personalizado",
+ "save": "Salvar",
+ "your_domain": "Seu domínio",
+ "custom_domain_error": "Você não pode adicionar um domínio de nível superior como domínio personalizado. Em vez disso, use um subdomínio como 'cursos.[seusite].com' ou 'www.[seusite].com'.",
+ "custom_domain_success": "Domínio personalizado adicionado com sucesso",
+ "dns_description": "Defina o seguinte registro em seu provedor DNS para continuar:",
+ "dns_type": "Tipo",
+ "dns_value": "Valor",
+ "dns_name": "Nome",
+ "refresh": "Atualizar",
+ "remove": "Remover",
+ "custom_favicon": "Favicon personalizado",
+ "custom_code": "Código personalizado"
},
"customize-lms": {
"title": "Personalizar LMS",
@@ -1084,7 +1096,8 @@
"learn": "Saber mais",
"month": "mês",
"monthly": "Por mês",
- "annually": "Anualmente"
+ "annually": "Anualmente",
+ "save": "Economize 2 meses"
},
"plan_names": {
"free": "Livre",
@@ -1206,9 +1219,9 @@
"update_organization": "Atualizar organização",
"team": {
"heading": "Equipe",
- "sub_heading": "Configure seu site",
"body": "Adicione colegas de equipe à sua organização para que ambos possam colaborar em projetos.",
- "button": "Gerenciar equipe"
+ "button": "Gerenciar equipe",
+ "sub_heading": "Monte sua equipe"
},
"custom_domain": {
"heading": "Domínio personalizado",
@@ -1239,7 +1252,10 @@
},
"profile_picture": {
"heading": "Foto do perfil",
- "upload_image": "Enviar Imagem"
+ "upload_image": "Enviar Imagem",
+ "file_size": "Tamanho máximo do arquivo:",
+ "accepted": "Aceito:",
+ "validation_error": "O tamanho do arquivo excede o limite máximo:"
}
},
"tabs": {
diff --git a/apps/dashboard/src/lib/utils/translations/ru.json b/apps/dashboard/src/lib/utils/translations/ru.json
index 2ab48f38..d50be0ac 100644
--- a/apps/dashboard/src/lib/utils/translations/ru.json
+++ b/apps/dashboard/src/lib/utils/translations/ru.json
@@ -194,7 +194,8 @@
"team_members": "Члены команды",
"back": "Вернуться к настройкам",
"download_lessons": "Обновите, чтобы скачивать уроки",
- "download_course": "Обновите, чтобы загрузить курс"
+ "download_course": "Обновите, чтобы загрузить курс",
+ "domain": "Обновите свой план, чтобы добавить персональные домены"
},
"audience": {
"title": "Аудитория",
@@ -923,12 +924,23 @@
"settings": {
"domains": {
"add": "Добавлять",
- "work_together": "Добавьте своих товарищей по команде или сотрудников в свою организацию. Начните работать вместе",
"url": "URL-адрес",
"update": "Обновить домен",
"custom": "Пользовательский домен",
"domain": "Добавьте свое собственное доменное имя",
- "title": "Пользовательский домен"
+ "title": "Пользовательский домен",
+ "save": "Сохранять",
+ "your_domain": "Ваш домен",
+ "custom_domain_error": "Вы не можете добавить домен верхнего уровня в качестве личного домена. Вместо этого используйте поддомен, например «courses.[вашсайт].com» или «www.[вашсайт].com».",
+ "custom_domain_success": "Персональный домен успешно добавлен",
+ "dns_description": "Чтобы продолжить, установите следующую запись у своего DNS-провайдера:",
+ "dns_type": "Тип",
+ "dns_value": "Ценить",
+ "dns_name": "Имя",
+ "refresh": "Обновить",
+ "remove": "Удалять",
+ "custom_favicon": "Пользовательский значок",
+ "custom_code": "Пользовательский код"
},
"customize-lms": {
"title": "Настроить систему управления обучением",
@@ -1084,7 +1096,8 @@
"learn": "Узнать больше",
"month": "месяц",
"monthly": "Ежемесячно",
- "annually": "Ежегодно"
+ "annually": "Ежегодно",
+ "save": "Сэкономьте 2 месяца"
},
"plan_names": {
"free": "Бесплатно",
@@ -1206,9 +1219,9 @@
"update_organization": "Обновить организацию",
"team": {
"heading": "Команда",
- "sub_heading": "Настройте свой веб-сайт",
"body": "Добавьте коллег в свою организацию, чтобы вы оба могли совместно работать над проектами.",
- "button": "Управление командой"
+ "button": "Управление командой",
+ "sub_heading": "Создайте свою команду"
},
"custom_domain": {
"heading": "Пользовательский домен",
@@ -1239,7 +1252,10 @@
},
"profile_picture": {
"heading": "Изображение профиля",
- "upload_image": "Загрузить изображение"
+ "upload_image": "Загрузить изображение",
+ "file_size": "Максимальный размер файла:",
+ "accepted": "Принял:",
+ "validation_error": "Размер файла превышает максимальный предел:"
}
},
"tabs": {
diff --git a/apps/dashboard/src/lib/utils/translations/vi.json b/apps/dashboard/src/lib/utils/translations/vi.json
index 75d6111f..7126db11 100644
--- a/apps/dashboard/src/lib/utils/translations/vi.json
+++ b/apps/dashboard/src/lib/utils/translations/vi.json
@@ -194,7 +194,8 @@
"team_members": "Thành viên của nhóm",
"back": "Quay lại Cài đặt",
"download_lessons": "Nâng cấp để tải bài học",
- "download_course": "Nâng cấp để tải xuống khóa học"
+ "download_course": "Nâng cấp để tải xuống khóa học",
+ "domain": "Nâng cấp gói của bạn để thêm miền tùy chỉnh"
},
"audience": {
"title": "Khán giả",
@@ -923,12 +924,23 @@
"settings": {
"domains": {
"add": "Thêm vào",
- "work_together": "Thêm đồng đội hoặc cộng tác viên vào tổ chức của bạn. Bắt đầu làm việc cùng nhau",
"url": "URL",
"update": "Cập nhật tên miền",
"custom": "Tên miền tùy chọn",
"domain": "Thêm tên miền của riêng bạn",
- "title": "Tên miền tùy chọn"
+ "title": "Tên miền tùy chọn",
+ "save": "Cứu",
+ "your_domain": "Tên miền của bạn",
+ "custom_domain_error": "Bạn không thể thêm miền cấp cao nhất làm miền tùy chỉnh. Thay vào đó, hãy sử dụng tên miền phụ như 'courses.[yourwebsite].com' hoặc 'www.[yourwebsite].com'.",
+ "custom_domain_success": "Đã thêm miền tùy chỉnh thành công",
+ "dns_description": "Đặt bản ghi sau trên nhà cung cấp DNS của bạn để tiếp tục:",
+ "dns_type": "Kiểu",
+ "dns_value": "Giá trị",
+ "dns_name": "Tên",
+ "refresh": "Làm cho khỏe lại",
+ "remove": "Di dời",
+ "custom_favicon": "Biểu tượng yêu thích tùy chỉnh",
+ "custom_code": "Mã tùy chỉnh"
},
"customize-lms": {
"title": "Tùy chỉnh LMS",
@@ -1084,7 +1096,8 @@
"learn": "Tìm hiểu thêm",
"month": "tháng",
"monthly": "hàng tháng",
- "annually": "Hàng năm"
+ "annually": "Hàng năm",
+ "save": "Tiết kiệm 2 tháng"
},
"plan_names": {
"free": "Miễn phí",
@@ -1206,9 +1219,9 @@
"update_organization": "Cập nhật tổ chức",
"team": {
"heading": "Đội",
- "sub_heading": "Thiết lập trang web của bạn",
"body": "Thêm đồng đội vào tổ chức của bạn để cả hai có thể cộng tác trong các dự án.",
- "button": "Quản lý nhóm"
+ "button": "Quản lý nhóm",
+ "sub_heading": "Thiết lập nhóm của bạn"
},
"custom_domain": {
"heading": "Tên miền tùy chọn",
@@ -1239,7 +1252,10 @@
},
"profile_picture": {
"heading": "Ảnh đại diện",
- "upload_image": "Tải hình ảnh lên"
+ "upload_image": "Tải hình ảnh lên",
+ "file_size": "Kích thước tệp tối đa:",
+ "accepted": "Đã chấp nhận:",
+ "validation_error": "Kích thước tệp vượt quá giới hạn tối đa:"
}
},
"tabs": {
diff --git a/apps/dashboard/src/lib/utils/types/org.ts b/apps/dashboard/src/lib/utils/types/org.ts
index 040f5da9..4bdd2ce0 100644
--- a/apps/dashboard/src/lib/utils/types/org.ts
+++ b/apps/dashboard/src/lib/utils/types/org.ts
@@ -34,6 +34,10 @@ export interface CurrentOrg {
is_active: boolean;
}[];
is_restricted: boolean;
+ customDomain?: string;
+ isCustomDomainVerified?: boolean;
+ customCode?: string;
+ favicon?: string;
}
export interface OrgTeamMember {
@@ -53,3 +57,62 @@ export interface OrgAudience {
avatar_url: string;
date_joined: string;
}
+
+export type DomainVerificationStatusProps =
+ | 'Valid Configuration'
+ | 'Invalid Configuration'
+ | 'Pending Verification'
+ | 'Domain Not Found'
+ | 'Unknown Error';
+
+// From https://vercel.com/docs/rest-api/endpoints#get-a-project-domain
+export interface DomainResponse {
+ name: string;
+ apexName: string;
+ projectId: string;
+ redirect?: string | null;
+ redirectStatusCode?: (307 | 301 | 302 | 308) | null;
+ gitBranch?: string | null;
+ updatedAt?: number;
+ createdAt?: number;
+ /** `true` if the domain is verified for use with the project. If `false` it will not be used as an alias on this project until the challenge in `verification` is completed. */
+ verified: boolean;
+ /** A list of verification challenges, one of which must be completed to verify the domain for use on the project. After the challenge is complete `POST /projects/:idOrName/domains/:domain/verify` to verify the domain. Possible challenges: - If `verification.type = TXT` the `verification.domain` will be checked for a TXT record matching `verification.value`. */
+ verification: {
+ type: string;
+ domain: string;
+ value: string;
+ reason: string;
+ }[];
+}
+
+// From https://vercel.com/docs/rest-api/endpoints#get-a-domain-s-configuration
+export interface DomainConfigResponse {
+ /** How we see the domain's configuration. - `CNAME`: Domain has a CNAME pointing to Vercel. - `A`: Domain's A record is resolving to Vercel. - `http`: Domain is resolving to Vercel but may be behind a Proxy. - `null`: Domain is not resolving to Vercel. */
+ configuredBy?: ('CNAME' | 'A' | 'http') | null;
+ /** Which challenge types the domain can use for issuing certs. */
+ acceptedChallenges?: ('dns-01' | 'http-01')[];
+ /** Whether or not the domain is configured AND we can automatically generate a TLS certificate. */
+ misconfigured: boolean;
+}
+
+// From https://vercel.com/docs/rest-api/endpoints#verify-project-domain
+export interface DomainVerificationResponse {
+ name: string;
+ apexName: string;
+ projectId: string;
+ redirect?: string | null;
+ redirectStatusCode?: (307 | 301 | 302 | 308) | null;
+ gitBranch?: string | null;
+ updatedAt?: number;
+ createdAt?: number;
+ /** `true` if the domain is verified for use with the project. If `false` it will not be used as an alias on this project until the challenge in `verification` is completed. */
+ verified: boolean;
+ /** A list of verification challenges, one of which must be completed to verify the domain for use on the project. After the challenge is complete `POST /projects/:idOrName/domains/:domain/verify` to verify the domain. Possible challenges: - If `verification.type = TXT` the `verification.domain` will be checked for a TXT record matching `verification.value`. */
+ verification?: {
+ type: string;
+ domain: string;
+ value: string;
+ reason: string;
+ }[];
+}
diff --git a/apps/dashboard/src/routes/+layout.server.ts b/apps/dashboard/src/routes/+layout.server.ts
index 67dd447c..b2b6a0d8 100644
--- a/apps/dashboard/src/routes/+layout.server.ts
+++ b/apps/dashboard/src/routes/+layout.server.ts
@@ -70,11 +70,19 @@ export const load = async ({ url, cookies }): Promise => {
const isDev = dev || isLocalHost;
if (!url.host.includes('.classroomio.com') && !isLocalHost) {
- // TODO: We can verify if custom domain here
- return response;
- }
+ // Custom domain
+ response.org = (await getCurrentOrg(url.host, true, true)) || null;
- if (!blockedSubdomain.includes(subdomain)) {
+ console.log('custom domain response.org', response.org);
+
+ if (!response.org) {
+ throw redirect(307, 'https://app.classroomio.com/404?type=org');
+ }
+
+ response.isOrgSite = true;
+ response.orgSiteName = response.org?.siteName || '';
+ return response;
+ } else if (!blockedSubdomain.includes(subdomain)) {
const answer = !!subdomain;
response.isOrgSite = debugMode || answer;
diff --git a/apps/dashboard/src/routes/api/domain/+server.ts b/apps/dashboard/src/routes/api/domain/+server.ts
new file mode 100644
index 00000000..e178100e
--- /dev/null
+++ b/apps/dashboard/src/routes/api/domain/+server.ts
@@ -0,0 +1,54 @@
+import { json } from '@sveltejs/kit';
+import { getSupabase } from '$lib/utils/functions/supabase';
+import {
+ getConfigResponse,
+ addDomainToVercel,
+ removeDomainFromVercelProject
+} from '$lib/utils/functions/domains';
+
+const supabase = getSupabase();
+
+export async function POST({ request }) {
+ const { params } = await request.json();
+ const accessToken = request.headers.get('Authorization');
+ console.log('/GET api/domain', params);
+
+ if (!params?.key || !accessToken) {
+ return json({ success: false, message: 'Missing fields' }, { status: 400 });
+ }
+
+ let user;
+ try {
+ const { data } = await supabase.auth.getUser(accessToken);
+ user = data.user;
+ } catch (error) {
+ console.error(error);
+ }
+
+ if (!user) {
+ return json({ success: false, message: 'Unauthenticated user' }, { status: 401 });
+ }
+
+ try {
+ switch (params.key) {
+ case 'verify_domain': {
+ const configResponse = await getConfigResponse(params.domain);
+
+ return json({ success: true, verified: !configResponse.misconfigured }, { status: 200 });
+ }
+ case 'add_domain': {
+ const addDomainResponse = await addDomainToVercel(params.domain);
+ return json({ success: true, data: addDomainResponse }, { status: 200 });
+ }
+ case 'remove_domain': {
+ const removeDomainResponse = await removeDomainFromVercelProject(params.domain);
+ return json({ success: true, data: removeDomainResponse }, { status: 200 });
+ }
+ default:
+ return json({ success: false, message: 'Invalid key' }, { status: 400 });
+ }
+ } catch (error) {
+ console.error(error);
+ return json({ success: false, message: 'Internal server error' }, { status: 500 });
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 63aecfb0..755232b3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -167,7 +167,7 @@ importers:
version: 5.1.6
vite:
specifier: ^4.4.2
- version: 4.4.8(@types/node@22.5.1)(sass@1.64.2)
+ version: 4.4.8(@types/node@18.11.10)(sass@1.64.2)
apps/dashboard:
dependencies:
@@ -249,6 +249,9 @@ importers:
html-to-image:
specifier: ^1.11.11
version: 1.11.11
+ is-valid-domain:
+ specifier: ^0.1.6
+ version: 0.1.6
js-yaml:
specifier: ^4.1.0
version: 4.1.0
@@ -294,6 +297,9 @@ importers:
sveltekit-i18n:
specifier: ^2.4.2
version: 2.4.2(svelte@4.1.2)
+ tldts:
+ specifier: ^6.1.41
+ version: 6.1.41
unsplash-js:
specifier: ^7.0.18
version: 7.0.18
@@ -369,7 +375,7 @@ importers:
version: 3.1.1
jest:
specifier: ^29.7.0
- version: 29.7.0(@types/node@22.5.1)(ts-node@10.9.2)
+ version: 29.7.0(@types/node@18.11.10)(ts-node@10.9.2)
postcss:
specifier: ^8.4.24
version: 8.4.24
@@ -408,7 +414,7 @@ importers:
version: 29.1.1(@babel/core@7.22.9)(babel-jest@29.7.0)(jest@29.7.0)(typescript@5.1.6)
ts-node:
specifier: ^10.9.2
- version: 10.9.2(@types/node@22.5.1)(typescript@5.1.6)
+ version: 10.9.2(@types/node@18.11.10)(typescript@5.1.6)
tsconfig:
specifier: workspace:*
version: link:../../packages/tsconfig
@@ -420,7 +426,7 @@ importers:
version: 5.1.6
vite:
specifier: ^4.4.8
- version: 4.4.8(@types/node@22.5.1)(sass@1.64.2)
+ version: 4.4.8(@types/node@18.11.10)(sass@1.64.2)
apps/docs:
dependencies:
@@ -5831,7 +5837,7 @@ packages:
sirv: 2.0.3
svelte: 4.1.2
undici: 5.22.1
- vite: 4.4.8(@types/node@22.5.1)(sass@1.64.2)
+ vite: 4.4.8(@types/node@18.11.10)(sass@1.64.2)
transitivePeerDependencies:
- supports-color
dev: true
@@ -5859,7 +5865,7 @@ packages:
svelte: 4.1.2
tiny-glob: 0.2.9
undici: 5.26.5
- vite: 4.4.8(@types/node@22.5.1)(sass@1.64.2)
+ vite: 4.4.8(@types/node@18.11.10)(sass@1.64.2)
transitivePeerDependencies:
- supports-color
@@ -5874,7 +5880,7 @@ packages:
'@sveltejs/vite-plugin-svelte': 2.5.3(svelte@4.1.2)(vite@4.4.8)
debug: 4.3.4(supports-color@5.5.0)
svelte: 4.1.2
- vite: 4.4.8(@types/node@22.5.1)(sass@1.64.2)
+ vite: 4.4.8(@types/node@18.11.10)(sass@1.64.2)
transitivePeerDependencies:
- supports-color
@@ -5892,7 +5898,7 @@ packages:
magic-string: 0.30.5
svelte: 4.1.2
svelte-hmr: 0.15.3(svelte@4.1.2)
- vite: 4.4.8(@types/node@22.5.1)(sass@1.64.2)
+ vite: 4.4.8(@types/node@18.11.10)(sass@1.64.2)
vitefu: 0.2.5(vite@4.4.8)
transitivePeerDependencies:
- supports-color
@@ -5975,7 +5981,7 @@ packages:
chalk: 3.0.0
css.escape: 1.5.1
dom-accessibility-api: 0.5.16
- jest: 29.7.0(@types/node@22.5.1)(ts-node@10.9.2)
+ jest: 29.7.0(@types/node@18.11.10)(ts-node@10.9.2)
lodash: 4.17.21
redent: 3.0.0
dev: true
@@ -6188,11 +6194,6 @@ packages:
/@types/node@18.11.10:
resolution: {integrity: sha512-juG3RWMBOqcOuXC643OAdSA525V44cVgGV6dUDuiFtss+8Fk5x1hI93Rsld43VeJVIeqlP9I7Fn9/qaVqoEAuQ==}
- /@types/node@22.5.1:
- resolution: {integrity: sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==}
- dependencies:
- undici-types: 6.19.8
-
/@types/nodemailer@6.4.11:
resolution: {integrity: sha512-Ld2c0frwpGT4VseuoeboCXQ7UJIkK3X7Lx/4YsZEiUHtHsthWAOCYtf6PAiLhMtfwV0cWJRabLBS3+LD8x6Nrw==}
dependencies:
@@ -8233,7 +8234,7 @@ packages:
path-type: 4.0.0
dev: false
- /create-jest@29.7.0(@types/node@22.5.1)(ts-node@10.9.2):
+ /create-jest@29.7.0(@types/node@18.11.10)(ts-node@10.9.2):
resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
@@ -8242,7 +8243,7 @@ packages:
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.11
- jest-config: 29.7.0(@types/node@22.5.1)(ts-node@10.9.2)
+ jest-config: 29.7.0(@types/node@18.11.10)(ts-node@10.9.2)
jest-util: 29.7.0
prompts: 2.4.2
transitivePeerDependencies:
@@ -11497,6 +11498,12 @@ packages:
engines: {node: '>=10'}
dev: true
+ /is-valid-domain@0.1.6:
+ resolution: {integrity: sha512-ZKtq737eFkZr71At8NxOFcP9O1K89gW3DkdrGMpp1upr/ueWjj+Weh4l9AI4rN0Gt8W2M1w7jrG2b/Yv83Ljpg==}
+ dependencies:
+ punycode: 2.3.1
+ dev: false
+
/is-weakmap@2.0.1:
resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==}
dev: true
@@ -11665,7 +11672,7 @@ packages:
- supports-color
dev: true
- /jest-cli@29.7.0(@types/node@22.5.1)(ts-node@10.9.2):
+ /jest-cli@29.7.0(@types/node@18.11.10)(ts-node@10.9.2):
resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
@@ -11679,10 +11686,10 @@ packages:
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
chalk: 4.1.2
- create-jest: 29.7.0(@types/node@22.5.1)(ts-node@10.9.2)
+ create-jest: 29.7.0(@types/node@18.11.10)(ts-node@10.9.2)
exit: 0.1.2
import-local: 3.1.0
- jest-config: 29.7.0(@types/node@22.5.1)(ts-node@10.9.2)
+ jest-config: 29.7.0(@types/node@18.11.10)(ts-node@10.9.2)
jest-util: 29.7.0
jest-validate: 29.7.0
yargs: 17.7.2
@@ -11728,48 +11735,7 @@ packages:
pretty-format: 29.7.0
slash: 3.0.0
strip-json-comments: 3.1.1
- ts-node: 10.9.2(@types/node@22.5.1)(typescript@5.1.6)
- transitivePeerDependencies:
- - babel-plugin-macros
- - supports-color
- dev: true
-
- /jest-config@29.7.0(@types/node@22.5.1)(ts-node@10.9.2):
- resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==}
- engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
- peerDependencies:
- '@types/node': '*'
- ts-node: '>=9.0.0'
- peerDependenciesMeta:
- '@types/node':
- optional: true
- ts-node:
- optional: true
- dependencies:
- '@babel/core': 7.23.3
- '@jest/test-sequencer': 29.7.0
- '@jest/types': 29.6.3
- '@types/node': 22.5.1
- babel-jest: 29.7.0(@babel/core@7.23.3)
- chalk: 4.1.2
- ci-info: 3.9.0
- deepmerge: 4.3.1
- glob: 7.2.3
- graceful-fs: 4.2.11
- jest-circus: 29.7.0
- jest-environment-node: 29.7.0
- jest-get-type: 29.6.3
- jest-regex-util: 29.6.3
- jest-resolve: 29.7.0
- jest-runner: 29.7.0
- jest-util: 29.7.0
- jest-validate: 29.7.0
- micromatch: 4.0.5
- parse-json: 5.2.0
- pretty-format: 29.7.0
- slash: 3.0.0
- strip-json-comments: 3.1.1
- ts-node: 10.9.2(@types/node@22.5.1)(typescript@5.1.6)
+ ts-node: 10.9.2(@types/node@18.11.10)(typescript@5.1.6)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
@@ -12058,7 +12024,7 @@ packages:
supports-color: 8.1.1
dev: true
- /jest@29.7.0(@types/node@22.5.1)(ts-node@10.9.2):
+ /jest@29.7.0(@types/node@18.11.10)(ts-node@10.9.2):
resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
@@ -12071,7 +12037,7 @@ packages:
'@jest/core': 29.7.0(ts-node@10.9.2)
'@jest/types': 29.6.3
import-local: 3.1.0
- jest-cli: 29.7.0(@types/node@22.5.1)(ts-node@10.9.2)
+ jest-cli: 29.7.0(@types/node@18.11.10)(ts-node@10.9.2)
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@@ -14335,7 +14301,7 @@ packages:
dependencies:
lilconfig: 2.1.0
postcss: 8.4.24
- ts-node: 10.9.2(@types/node@22.5.1)(typescript@5.1.6)
+ ts-node: 10.9.2(@types/node@18.11.10)(typescript@5.1.6)
yaml: 2.3.4
dev: true
@@ -14353,7 +14319,7 @@ packages:
dependencies:
lilconfig: 2.1.0
postcss: 8.4.32
- ts-node: 10.9.2(@types/node@22.5.1)(typescript@5.1.6)
+ ts-node: 10.9.2(@types/node@18.11.10)(typescript@5.1.6)
yaml: 2.3.4
/postcss-nested@6.0.1(postcss@8.4.32):
@@ -14692,7 +14658,6 @@ packages:
/punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
- dev: true
/puppeteer-core@20.9.0(typescript@5.3.2):
resolution: {integrity: sha512-H9fYZQzMTRrkboEfPmf7m3CLDN6JvbxXA3qTtS+dFt27tR+CsFHzPsT6pzp6lYL6bJbAPaR0HaPO6uSi+F94Pg==}
@@ -16146,7 +16111,7 @@ packages:
jest: '>= 27'
svelte: '>= 3'
dependencies:
- jest: 29.7.0(@types/node@22.5.1)(ts-node@10.9.2)
+ jest: 29.7.0(@types/node@18.11.10)(ts-node@10.9.2)
svelte: 4.1.2
dev: true
@@ -16575,6 +16540,17 @@ packages:
engines: {node: '>=12'}
dev: true
+ /tldts-core@6.1.41:
+ resolution: {integrity: sha512-SkwZgo1ZzMp2ziMBwci5VBnLR9VywCi02jSgMX5TO5kf9fdaBsxZkblLff3NlJNTcH0vfvEsgw2B7jVR556Vgw==}
+ dev: false
+
+ /tldts@6.1.41:
+ resolution: {integrity: sha512-RNpUkL5fYD2DTQQCdr8QMDp6UL0ThtpXT3q3+qPE05dIT+RK2I3M0VByVbQN1dEhLUGzimivVwxK2By9epLk6w==}
+ hasBin: true
+ dependencies:
+ tldts-core: 6.1.41
+ dev: false
+
/tmp@0.2.3:
resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==}
engines: {node: '>=14.14'}
@@ -16700,7 +16676,7 @@ packages:
babel-jest: 29.7.0(@babel/core@7.22.9)
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
- jest: 29.7.0(@types/node@22.5.1)(ts-node@10.9.2)
+ jest: 29.7.0(@types/node@18.11.10)(ts-node@10.9.2)
jest-util: 29.7.0
json5: 2.2.3
lodash.memoize: 4.1.2
@@ -16710,7 +16686,7 @@ packages:
yargs-parser: 21.1.1
dev: true
- /ts-node@10.9.2(@types/node@22.5.1)(typescript@5.1.6):
+ /ts-node@10.9.2(@types/node@18.11.10)(typescript@5.1.6):
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
hasBin: true
peerDependencies:
@@ -16729,7 +16705,7 @@ packages:
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
- '@types/node': 22.5.1
+ '@types/node': 18.11.10
acorn: 8.11.2
acorn-walk: 8.3.1
arg: 4.1.3
@@ -16988,9 +16964,6 @@ packages:
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
dev: false
- /undici-types@6.19.8:
- resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
-
/undici@5.22.1:
resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==}
engines: {node: '>=14.0'}
@@ -17390,7 +17363,7 @@ packages:
vfile-message: 4.0.2
dev: false
- /vite@4.4.8(@types/node@22.5.1)(sass@1.64.2):
+ /vite@4.4.8(@types/node@18.11.10)(sass@1.64.2):
resolution: {integrity: sha512-LONawOUUjxQridNWGQlNizfKH89qPigK36XhMI7COMGztz8KNY0JHim7/xDd71CZwGT4HtSRgI7Hy+RlhG0Gvg==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
@@ -17418,7 +17391,7 @@ packages:
terser:
optional: true
dependencies:
- '@types/node': 22.5.1
+ '@types/node': 18.11.10
esbuild: 0.18.20
postcss: 8.4.32
rollup: 3.29.4
@@ -17434,7 +17407,7 @@ packages:
vite:
optional: true
dependencies:
- vite: 4.4.8(@types/node@22.5.1)(sass@1.64.2)
+ vite: 4.4.8(@types/node@18.11.10)(sass@1.64.2)
/vscode-oniguruma@1.7.0:
resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
diff --git a/supabase/migrations/20240904050707_custom_domain.sql b/supabase/migrations/20240904050707_custom_domain.sql
new file mode 100644
index 00000000..2bf1be78
--- /dev/null
+++ b/supabase/migrations/20240904050707_custom_domain.sql
@@ -0,0 +1,11 @@
+alter table "public"."organization" add column "customCode" text;
+
+alter table "public"."organization" add column "customDomain" text;
+
+alter table "public"."organization" add column "favicon" text;
+
+alter table "public"."organization" add column "isCustomDomainVerified" boolean default false;
+
+CREATE UNIQUE INDEX "organization_customDomain_key" ON public.organization USING btree ("customDomain");
+
+alter table "public"."organization" add constraint "organization_customDomain_key" UNIQUE using index "organization_customDomain_key";