feat: automatically detect language on server and translate UI (#530)

* feat: detect language on the server and translate page

* translate auth pages

* PR reviews
This commit is contained in:
Rotimi Best
2024-12-28 23:11:55 +01:00
committed by GitHub
parent 1f1ee8afd3
commit 381776f388
24 changed files with 431 additions and 232 deletions
+1 -1
View File
@@ -19,6 +19,6 @@
"deno.importMap": "./supabase/functions/import_map.json", "deno.importMap": "./supabase/functions/import_map.json",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": true "source.organizeImports": "explicit"
} }
} }
@@ -1,11 +1,12 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import GoogleIconColored from '../Icons/GoogleIconColored.svelte';
import Avatar from '$lib/components/Avatar/index.svelte'; import Avatar from '$lib/components/Avatar/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { t } from '$lib/utils/functions/translations';
import { currentOrg } from '$lib/utils/store/org'; import { currentOrg } from '$lib/utils/store/org';
import type { SupabaseClient } from '@supabase/supabase-js'; import type { SupabaseClient } from '@supabase/supabase-js';
import GoogleIconColored from '../Icons/GoogleIconColored.svelte';
export let supabase: SupabaseClient; export let supabase: SupabaseClient;
export let handleSubmit = () => {}; export let handleSubmit = () => {};
@@ -47,11 +48,11 @@
} }
</script> </script>
<div class="app-background w-full min-h-screen flex items-center justify-center"> <div class="app-background flex min-h-screen w-full items-center justify-center">
<div class="container border border-gray bg-white dark:bg-black"> <div class="border-gray container border bg-white dark:bg-black">
<div class="flex items-center flex-col p-2 lg:px-8 lg:py-3"> <div class="flex flex-col items-center p-2 lg:px-8 lg:py-3">
{#if !showOnlyContent || showLogo} {#if !showOnlyContent || showLogo}
<div class="flex flex-col items-center justify-center w-full pt-2"> <div class="flex w-full flex-col items-center justify-center pt-2">
<Avatar <Avatar
src={$currentOrg.avatar_url ? $currentOrg.avatar_url : '/logo-192.png'} src={$currentOrg.avatar_url ? $currentOrg.avatar_url : '/logo-192.png'}
name={$currentOrg.name ? $currentOrg.name : 'ClassroomIO'} name={$currentOrg.name ? $currentOrg.name : 'ClassroomIO'}
@@ -61,7 +62,7 @@
className="mr-2" className="mr-2"
/> />
<a href="/"> <a href="/">
<h4 class="dark:text-white text-xl mt-0"> <h4 class="mt-0 text-xl dark:text-white">
{$currentOrg.name ? $currentOrg.name : 'ClassroomIO'} {$currentOrg.name ? $currentOrg.name : 'ClassroomIO'}
</h4> </h4>
</a> </a>
@@ -70,13 +71,13 @@
<form <form
bind:this={formRef} bind:this={formRef}
on:submit|preventDefault={handleSubmit} on:submit|preventDefault={handleSubmit}
class="flex items-center flex-col w-10/12" class="flex w-10/12 flex-col items-center"
> >
<slot /> <slot />
</form> </form>
{#if !showOnlyContent && !hideGoogleAuth} {#if !showOnlyContent && !hideGoogleAuth}
<div class="w-10/12 mb-3"> <div class="mb-3 w-10/12">
<p class="dark:text-white text-sm mb-5">or sign up with:</p> <p class="mb-5 text-sm dark:text-white">{$t('login.signup_with')}:</p>
<PrimaryButton <PrimaryButton
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
onClick={signInWithGoogle} onClick={signInWithGoogle}
@@ -84,22 +85,24 @@
className="py-3 sm:w-full w-full" className="py-3 sm:w-full w-full"
> >
<GoogleIconColored /> <GoogleIconColored />
<span class="ml-2">{isLogin ? 'Sign in' : 'Sign up'} with Google</span> <span class="ml-2">
{isLogin ? $t('login.login_with_google') : $t('login.signup_with_google')}
</span>
</PrimaryButton> </PrimaryButton>
</div> </div>
{/if} {/if}
</div> </div>
{#if !showOnlyContent} {#if !showOnlyContent}
<div class="w-full p-6 border-t border-grey text-center"> <div class="border-grey w-full border-t p-6 text-center">
{#if isLogin} {#if isLogin}
Not registered yet? <a {$t('login.not_registered_yet')}
class="hover:underline text-primary-700" <a class="text-primary-700 hover:underline" href="/signup{$page.url.search}"
href="/signup{$page.url.search}">Sign up</a >{$t('login.signup')}</a
> >
{:else} {:else}
Already have an account? <a {$t('login.already_have_account')}
class="hover:underline text-primary-700" <a class="text-primary-700 hover:underline" href="/login{$page.url.search}"
href="/login{$page.url.search}">Log In</a >{$t('login.login')}</a
> >
{/if} {/if}
</div> </div>
@@ -1,34 +1,34 @@
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition'; import { goto } from '$app/navigation';
import get from 'lodash/get';
import LogoFacebook from 'carbon-icons-svelte/lib/LogoFacebook.svelte';
import LogoTwitter from 'carbon-icons-svelte/lib/LogoTwitter.svelte';
import LogoLinkedin from 'carbon-icons-svelte/lib/LogoLinkedin.svelte';
import { Accordion, AccordionItem, Grid, Row, Column } from 'carbon-components-svelte';
import LocationFilled from 'carbon-icons-svelte/lib/LocationFilled.svelte';
import Phone from 'carbon-icons-svelte/lib/Phone.svelte';
import Email from 'carbon-icons-svelte/lib/Email.svelte';
import Rocket from 'carbon-icons-svelte/lib/Rocket.svelte';
import PageLoader from './PageLoader.svelte';
import { orgLandingpageValidation } from '$lib/utils/functions/validator';
import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import Navigation from '$lib/components/Navigation/index.svelte';
import Box from '$lib/components/Box/index.svelte'; import Box from '$lib/components/Box/index.svelte';
import Card from '$lib/components/Courses/components/Card/index.svelte'; import Card from '$lib/components/Courses/components/Card/index.svelte';
import CardLoader from '$lib/components/Courses/components/Card/Loader.svelte'; import CardLoader from '$lib/components/Courses/components/Card/Loader.svelte';
import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte'; import { courseMetaDeta, courses } from '$lib/components/Courses/store';
import { getSupabase } from '$lib/utils/functions/supabase';
import { courses, courseMetaDeta } from '$lib/components/Courses/store';
import { getCourseBySiteName } from '$lib/utils/services/org';
import { validateEmail } from '$lib/utils/functions/validateEmail';
import TextArea from '$lib/components/Form/TextArea.svelte'; import TextArea from '$lib/components/Form/TextArea.svelte';
import { goto } from '$app/navigation'; import TextField from '$lib/components/Form/TextField.svelte';
import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte';
import Navigation from '$lib/components/Navigation/index.svelte';
import { landingPageSettings } from '$lib/components/Org/Settings/store'; import { landingPageSettings } from '$lib/components/Org/Settings/store';
import { t } from '$lib/utils/functions/translations'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import PoweredBy from '$lib/components/Upgrade/PoweredBy.svelte'; import PoweredBy from '$lib/components/Upgrade/PoweredBy.svelte';
import { getSupabase } from '$lib/utils/functions/supabase';
import { t } from '$lib/utils/functions/translations';
import { validateEmail } from '$lib/utils/functions/validateEmail';
import { orgLandingpageValidation } from '$lib/utils/functions/validator';
import { getCourseBySiteName } from '$lib/utils/services/org';
import type { CurrentOrg } from '$lib/utils/types/org'; import type { CurrentOrg } from '$lib/utils/types/org';
import { Accordion, AccordionItem, Column, Grid, Row } from 'carbon-components-svelte';
import Email from 'carbon-icons-svelte/lib/Email.svelte';
import LocationFilled from 'carbon-icons-svelte/lib/LocationFilled.svelte';
import LogoFacebook from 'carbon-icons-svelte/lib/LogoFacebook.svelte';
import LogoLinkedin from 'carbon-icons-svelte/lib/LogoLinkedin.svelte';
import LogoTwitter from 'carbon-icons-svelte/lib/LogoTwitter.svelte';
import Phone from 'carbon-icons-svelte/lib/Phone.svelte';
import Rocket from 'carbon-icons-svelte/lib/Rocket.svelte';
import get from 'lodash/get';
import { fade } from 'svelte/transition';
import PageLoader from './PageLoader.svelte';
export let orgSiteName = ''; export let orgSiteName = '';
export let org: CurrentOrg | null; export let org: CurrentOrg | null;
@@ -115,7 +115,6 @@
if (!siteName) return; if (!siteName) return;
try { try {
console.log('sitename', siteName);
$courseMetaDeta.isLoading = true; $courseMetaDeta.isLoading = true;
const coursesResult = await getCourseBySiteName(siteName); const coursesResult = await getCourseBySiteName(siteName);
courses.set(coursesResult); courses.set(coursesResult);
@@ -182,7 +181,7 @@
{#if $landingPageSettings.header.show} {#if $landingPageSettings.header.show}
<header <header
id="header" id="header"
class={`w-full h-[100vh] md:h-[90vh] mb-10 relative ${ class={`relative mb-10 h-[100vh] w-full md:h-[90vh] ${
$landingPageSettings.header.background?.show $landingPageSettings.header.background?.show
? 'bg-cover bg-center' ? 'bg-cover bg-center'
: 'border-b border-gray-300' : 'border-b border-gray-300'
@@ -196,25 +195,25 @@
isOrgSite={true} isOrgSite={true}
/> />
<div class="absolute h-[100vh] md:h-[90vh] top-0 w-full opacity-80 z-10 bg-white" /> <div class="absolute top-0 z-10 h-[100vh] w-full bg-white opacity-80 md:h-[90vh]" />
{#if $landingPageSettings.header.banner.show} {#if $landingPageSettings.header.banner.show}
<div class="flex items-center justify-center md:h-full py-2"> <div class="flex items-center justify-center py-2 md:h-full">
<div <div
class="md:w-11/12 lg:w-5/6 w-full flex items-center justify-center md:justify-between flex-col-reverse md:flex-row z-20 relative" class="relative z-20 flex w-full flex-col-reverse items-center justify-center md:w-11/12 md:flex-row md:justify-between lg:w-5/6"
> >
<!-- Course Description --> <!-- Course Description -->
<div class="md:w-2/5 w-11/12 py-10 flex flex-col items-center md:items-start mr-3"> <div class="mr-3 flex w-11/12 flex-col items-center py-10 md:w-2/5 md:items-start">
<p class=" text-primary-600 text-2xl font-semibold capitalize"> <p class=" text-primary-600 text-2xl font-semibold capitalize">
{org.name} {org.name}
</p> </p>
<h1 <h1
class="text-4xl md:text-5xl lg:text-6xl text-center md:text-start my-4 font-bold" class="my-4 text-center text-4xl font-bold md:text-start md:text-5xl lg:text-6xl"
> >
{$landingPageSettings.header.title} <br /><span class="text-primary-600" {$landingPageSettings.header.title} <br /><span class="text-primary-600"
>{$landingPageSettings.header.titleHighlight}</span >{$landingPageSettings.header.titleHighlight}</span
> >
</h1> </h1>
<p class="text-md mb-3 md:mb-5 text-xl text-center md:text-start"> <p class="text-md mb-3 text-center text-xl md:mb-5 md:text-start">
{$landingPageSettings.header.subtitle} {$landingPageSettings.header.subtitle}
</p> </p>
@@ -228,7 +227,7 @@
/> />
</div> </div>
<div class="w-5/6 md:w-1/2 md:max-w-[650px] flex"> <div class="flex w-5/6 md:w-1/2 md:max-w-[650px]">
{#if isYouTubeLink($landingPageSettings.header?.banner?.video) && $landingPageSettings.header.banner.type === 'video'} {#if isYouTubeLink($landingPageSettings.header?.banner?.video) && $landingPageSettings.header.banner.type === 'video'}
<div bind:this={player} id="player" style="width:100%; border-radius:12px"> <div bind:this={player} id="player" style="width:100%; border-radius:12px">
<iframe <iframe
@@ -256,10 +255,10 @@
</div> </div>
</div> </div>
{:else} {:else}
<div class="w-full h-full flex items-center justify-center md:flex-row z-20 relative"> <div class="relative z-20 flex h-full w-full items-center justify-center md:flex-row">
<div class="max-w-[600px] mx-auto w-11/12 py-10 flex flex-col items-center"> <div class="mx-auto flex w-11/12 max-w-[600px] flex-col items-center py-10">
<p class=" text-primary-600 text-2xl font-semibold capitalize">{org.name}</p> <p class=" text-primary-600 text-2xl font-semibold capitalize">{org.name}</p>
<h1 class="text-4xl md:text-5xl lg:text-6xl text-center my-4 font-bold"> <h1 class="my-4 text-center text-4xl font-bold md:text-5xl lg:text-6xl">
{$landingPageSettings.header.title} <br /><span class="text-primary-600" {$landingPageSettings.header.title} <br /><span class="text-primary-600"
>{$landingPageSettings.header.titleHighlight}</span >{$landingPageSettings.header.titleHighlight}</span
> >
@@ -284,9 +283,9 @@
<!-- Our Story section --> <!-- Our Story section -->
{#if $landingPageSettings.aboutUs.show} {#if $landingPageSettings.aboutUs.show}
<section id="aboutus" class="m-h-[400px] my-10 mx-auto max-w-6xl w-full"> <section id="aboutus" class="m-h-[400px] mx-auto my-10 w-full max-w-6xl">
<div class="mx-4 flex items-center justify-evenly flex-col lg:flex-row"> <div class="mx-4 flex flex-col items-center justify-evenly lg:flex-row">
<div class="max-w-[600px] lg:w-2/5 mr-5 mb-5 lg:mb-0"> <div class="mb-5 mr-5 max-w-[600px] lg:mb-0 lg:w-2/5">
<h2 class="text-4xl md:text-5xl lg:text-6xl">{$landingPageSettings.aboutUs.title}</h2> <h2 class="text-4xl md:text-5xl lg:text-6xl">{$landingPageSettings.aboutUs.title}</h2>
<p class="mb-2"> <p class="mb-2">
{$landingPageSettings.aboutUs.content} {$landingPageSettings.aboutUs.content}
@@ -297,7 +296,7 @@
<img <img
src={$landingPageSettings.aboutUs.imageUrl} src={$landingPageSettings.aboutUs.imageUrl}
alt="Our Story" alt="Our Story"
class=" rounded-2xl max-h-[450px]" class=" max-h-[450px] rounded-2xl"
/> />
</div> </div>
</div> </div>
@@ -306,10 +305,10 @@
<!-- Courses Section --> <!-- Courses Section -->
{#if $landingPageSettings.courses.show} {#if $landingPageSettings.courses.show}
<section id="courses" transition:fade class="my-10 mx-auto max-w-6xl w-full"> <section id="courses" transition:fade class="mx-auto my-10 w-full max-w-6xl">
<div class="w-full"> <div class="w-full">
<div class="max-w-[500px] mx-auto w-11/12 py-10"> <div class="mx-auto w-11/12 max-w-[500px] py-10">
<h1 class="text-4xl md:text-5xl lg:text-6xl text-center my-4 font-bold"> <h1 class="my-4 text-center text-4xl font-bold md:text-5xl lg:text-6xl">
{$landingPageSettings.courses.title} {$landingPageSettings.courses.title}
<span class="text-primary-600">{$landingPageSettings.courses.titleHighlight}</span> <span class="text-primary-600">{$landingPageSettings.courses.titleHighlight}</span>
</h1> </h1>
@@ -319,13 +318,13 @@
</div> </div>
</div> </div>
{#if $courseMetaDeta.isLoading} {#if $courseMetaDeta.isLoading}
<div class="cards-container my-4 mx-2"> <div class="cards-container mx-2 my-4">
<CardLoader /> <CardLoader />
<CardLoader /> <CardLoader />
<CardLoader /> <CardLoader />
</div> </div>
{:else if $courses.length > 0} {:else if $courses.length > 0}
<div class="cards-container my-4 mx-2"> <div class="cards-container mx-2 my-4">
{#each $courses.slice(0, viewAll ? $courses.length : 3) as courseData} {#each $courses.slice(0, viewAll ? $courses.length : 3) as courseData}
<Card <Card
id={courseData.id} id={courseData.id}
@@ -350,17 +349,17 @@
{:else} {:else}
<Box> <Box>
<CoursesEmptyIcon /> <CoursesEmptyIcon />
<h3 class="dark:text-white text-2xl my-5"> <h3 class="my-5 text-2xl dark:text-white">
{$t('course.navItem.landing_page.no_course_published')} {$t('course.navItem.landing_page.no_course_published')}
</h3> </h3>
<p class="dark:text-white w-1/3 text-center"> <p class="w-1/3 text-center dark:text-white">
{$t('course.navItem.landing_page.coming_your_way')} {$t('course.navItem.landing_page.coming_your_way')}
</p> </p>
</Box> </Box>
{/if} {/if}
{#if $courses.length > 3} {#if $courses.length > 3}
<div class="w-full mt-3 flex justify-center"> <div class="mt-3 flex w-full justify-center">
<PrimaryButton <PrimaryButton
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
onClick={() => (viewAll = !viewAll)} onClick={() => (viewAll = !viewAll)}
@@ -376,9 +375,9 @@
<!-- FAQ Section --> <!-- FAQ Section -->
{#if $landingPageSettings.faq.show} {#if $landingPageSettings.faq.show}
<section id="faq" transition:fade class="my-10 mx-auto max-w-[700px] w-full"> <section id="faq" transition:fade class="mx-auto my-10 w-full max-w-[700px]">
<div class="py-10"> <div class="py-10">
<h1 class="text-4xl md:text-5xl lg:text-6xl text-center my-4 font-bold"> <h1 class="my-4 text-center text-4xl font-bold md:text-5xl lg:text-6xl">
{$landingPageSettings.faq.title} {$landingPageSettings.faq.title}
</h1> </h1>
</div> </div>
@@ -398,10 +397,10 @@
<!-- Contact Section--> <!-- Contact Section-->
{#if $landingPageSettings.contact.show} {#if $landingPageSettings.contact.show}
<section id="contact" transition:fade class="my-10 w-full bg-primary-50"> <section id="contact" transition:fade class="bg-primary-50 my-10 w-full">
<div class="mx-auto max-w-6xl w-full"> <div class="mx-auto w-full max-w-6xl">
<div class="max-w-[500px] mx-auto w-11/12 py-10"> <div class="mx-auto w-11/12 max-w-[500px] py-10">
<h1 class="text-4xl md:text-5xl lg:text-6xl text-center my-4 font-bold"> <h1 class="my-4 text-center text-4xl font-bold md:text-5xl lg:text-6xl">
{$landingPageSettings.contact.title} {$landingPageSettings.contact.title}
<span class="text-primary-600">{$landingPageSettings.contact.titleHighlight}</span> <span class="text-primary-600">{$landingPageSettings.contact.titleHighlight}</span>
</h1> </h1>
@@ -411,37 +410,37 @@
<!-- Contact Details --> <!-- Contact Details -->
<Row> <Row>
<Column <Column
class="flex items-center flex-col justify-center break-all text-center cursor-pointer hover:shadow-xl rounded-lg transition-all duration-500 py-2 mx-2" class="mx-2 flex cursor-pointer flex-col items-center justify-center break-all rounded-lg py-2 text-center transition-all duration-500 hover:shadow-xl"
> >
<LocationFilled size={32} /> <LocationFilled size={32} />
<p class="text-xs md:text-sm mt-3 max-w-[200px]"> <p class="mt-3 max-w-[200px] text-xs md:text-sm">
{$landingPageSettings.contact.address} {$landingPageSettings.contact.address}
</p> </p>
</Column> </Column>
<Column <Column
class="flex items-center flex-col justify-center break-all text-center cursor-pointer hover:shadow-xl rounded-lg transition-all duration-500 py-2 mx-2" class="mx-2 flex cursor-pointer flex-col items-center justify-center break-all rounded-lg py-2 text-center transition-all duration-500 hover:shadow-xl"
> >
<Phone size={32} /> <Phone size={32} />
<p class="text-xs md:text-sm mt-3">{$landingPageSettings.contact.phone}</p> <p class="mt-3 text-xs md:text-sm">{$landingPageSettings.contact.phone}</p>
</Column> </Column>
<Column <Column
class="flex items-center flex-col justify-center break-all text-center cursor-pointer hover:shadow-xl rounded-lg transition-all duration-500 py-2 mx-2" class="mx-2 flex cursor-pointer flex-col items-center justify-center break-all rounded-lg py-2 text-center transition-all duration-500 hover:shadow-xl"
> >
<Email size={32} /> <Email size={32} />
<p class="text-xs md:text-sm mt-3">{$landingPageSettings.contact.email}</p> <p class="mt-3 text-xs md:text-sm">{$landingPageSettings.contact.email}</p>
</Column> </Column>
</Row> </Row>
<!-- Contact Form --> <!-- Contact Form -->
<div class="mt-8 bg-white p-7 rounded-lg"> <div class="mt-8 rounded-lg bg-white p-7">
{#if successContactSaved} {#if successContactSaved}
<div class="w-full flex items-center justify-center"> <div class="flex w-full items-center justify-center">
{$t('course.navItem.landing_page.thank_you')} {$t('course.navItem.landing_page.thank_you')}
</div> </div>
{:else} {:else}
<form on:submit|preventDefault={handleContactSubmit}> <form on:submit|preventDefault={handleContactSubmit}>
<div class="w-full flex justify-between flex-col md:flex-row"> <div class="flex w-full flex-col justify-between md:flex-row">
<div class="w-full md:w-2/4 mr-5"> <div class="mr-5 w-full md:w-2/4">
<TextField <TextField
label={$t('course.navItem.landing_page.name')} label={$t('course.navItem.landing_page.name')}
bind:value={contact.name} bind:value={contact.name}
@@ -481,7 +480,7 @@
type="submit" type="submit"
isLoading={isContactSubmiting} isLoading={isContactSubmiting}
> >
<span class="mr-2 text-md">{$t('course.navItem.landing_page.submit')}</span> <span class="text-md mr-2">{$t('course.navItem.landing_page.submit')}</span>
<Rocket size={24} /> <Rocket size={24} />
</PrimaryButton> </PrimaryButton>
</form> </form>
@@ -494,12 +493,12 @@
<!-- Waitlist Section --> <!-- Waitlist Section -->
{#if $landingPageSettings.mailinglist.show} {#if $landingPageSettings.mailinglist.show}
<section id="waitlist" transition:fade class="my-10 mx-auto max-w-6xl w-[95%]"> <section id="waitlist" transition:fade class="mx-auto my-10 w-[95%] max-w-6xl">
<div <div
class="bg-primary-700 rounded-lg flex flex-col lg:flex-row lg:items-center px-4 md:px-10 py-14" class="bg-primary-700 flex flex-col rounded-lg px-4 py-14 md:px-10 lg:flex-row lg:items-center"
> >
<div class="w-full md:w-[65%] md:mr-4"> <div class="w-full md:mr-4 md:w-[65%]">
<h1 class="text-4xl font-bold mb-5 mt-0 text-white"> <h1 class="mb-5 mt-0 text-4xl font-bold text-white">
{$landingPageSettings.mailinglist.title} {$landingPageSettings.mailinglist.title}
</h1> </h1>
<p class="text-lg text-white"> <p class="text-lg text-white">
@@ -507,7 +506,7 @@
</p> </p>
</div> </div>
<form on:submit|preventDefault={handleSubmit} class="my-4 w-full md:w-fit"> <form on:submit|preventDefault={handleSubmit} class="my-4 w-full md:w-fit">
<div class="flex items-center flex-col sm:flex-row"> <div class="flex flex-col items-center sm:flex-row">
{#if success} {#if success}
<p class="text-white">{$t('course.navItem.landing_page.successful_sub')}</p> <p class="text-white">{$t('course.navItem.landing_page.successful_sub')}</p>
{:else} {:else}
@@ -538,9 +537,9 @@
{#if $landingPageSettings.footer.show} {#if $landingPageSettings.footer.show}
<footer <footer
id="footer" id="footer"
class="flex justify-center items-center flex-col my-10 w-full px-5 py-10 md:py-3 border-b-0 border-r-0 border-t border-l-0 border-gray-300" class="my-10 flex w-full flex-col items-center justify-center border-b-0 border-l-0 border-r-0 border-t border-gray-300 px-5 py-10 md:py-3"
> >
<ul class="flex w-11/12 items-center flex-col sm:flex-row"> <ul class="flex w-11/12 flex-col items-center sm:flex-row">
<div class="logo"> <div class="logo">
<a <a
href="/" href="/"
@@ -552,16 +551,16 @@
<img <img
src={org.avatar_url || '/logo-192.png'} src={org.avatar_url || '/logo-192.png'}
alt={`${org.name} logo`} alt={`${org.name} logo`}
class="rounded max-h-10 w-10 inline-block mx-auto" class="mx-auto inline-block max-h-10 w-10 rounded"
data-atf="1" data-atf="1"
/> />
<h3 class="text-black ml-3 text-xl">{org.name}</h3> <h3 class="ml-3 text-xl text-black">{org.name}</h3>
</a> </a>
</div> </div>
<span class="flex-grow" /> <span class="flex-grow" />
<div class="flex mt-5 sm:mt-0 gap-2"> <div class="mt-5 flex gap-2 sm:mt-0">
{#if $landingPageSettings.footer.facebook} {#if $landingPageSettings.footer.facebook}
<li> <li>
<a <a
@@ -2,13 +2,13 @@
import Modal from '$lib/components/Modal/index.svelte'; import Modal from '$lib/components/Modal/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
import { profile } from '$lib/utils/store/user'; import { t } from '$lib/utils/functions/translations';
import { import {
triggerSendEmail, NOTIFICATION_NAME,
NOTIFICATION_NAME triggerSendEmail
} from '$lib/utils/services/notification/notification'; } from '$lib/utils/services/notification/notification';
import { currentOrg } from '$lib/utils/store/org'; import { currentOrg } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations'; import { profile } from '$lib/utils/store/user';
const WAIT_SEC = 120; const WAIT_SEC = 120;
const WAIT_TIME = WAIT_SEC * 1000; const WAIT_TIME = WAIT_SEC * 1000;
@@ -51,21 +51,21 @@
}, WAIT_TIME); }, WAIT_TIME);
}; };
$: open = Boolean(!$profile.is_email_verified && !!$currentOrg.id); $: open = Boolean(!$profile.is_email_verified && !!$profile.id && !!$currentOrg.id);
$: open && sendVerificationCode(); $: open && sendVerificationCode();
</script> </script>
<Modal {open} isCloseable={false} width="w-4/5" maxWidth="w-[500px]" containerClass="p-4"> <Modal {open} isCloseable={false} width="w-4/5" maxWidth="w-[500px]" containerClass="p-4">
<div class="flex flex-col items-center space-y-6 text-center"> <div class="flex flex-col items-center space-y-6 text-center">
<img src="/verify-email.svg" alt="email verification" /> <img src="/verify-email.svg" alt="email verification" />
<p class="font-bold text-xl">{$t('verify_email_modal.heading')}</p> <p class="text-xl font-bold">{$t('verify_email_modal.heading')}</p>
<p class="text-sm w-[70%] text-gray-700 dark:text-gray-200"> <p class="w-[70%] text-sm text-gray-700 dark:text-gray-200">
{$t('verify_email_modal.sent_verification')} {$t('verify_email_modal.sent_verification')}
{$profile.email} {$profile.email}
{$t('verify_email_modal.to_confirm')} {$t('verify_email_modal.to_confirm')}
</p> </p>
<div class="flex items-center flex-col"> <div class="flex flex-col items-center">
<PrimaryButton <PrimaryButton
isDisabled={loading || isSent} isDisabled={loading || isSent}
className="font-normal" className="font-normal"
@@ -76,7 +76,10 @@ export async function getProfile({
} }
// Skip refetching profile, if already in store // Skip refetching profile, if already in store
if (profileStore.id) return; if (profileStore.id) {
handleLocaleChange(profileStore.locale);
return;
}
// Check if user has profile // Check if user has profile
let { let {
@@ -124,7 +127,7 @@ export async function getProfile({
setAnalyticsUser(); setAnalyticsUser();
// Fetch language // Fetch language
handleLocaleChange(profileStore.locale); handleLocaleChange(newProfileData[0].locale);
if (isOrgSite) { if (isOrgSite) {
const { data, error } = await supabase const { data, error } = await supabase
@@ -179,8 +182,7 @@ export async function getProfile({
// Set user in sentry // Set user in sentry
setAnalyticsUser(); setAnalyticsUser();
// Fetch language handleLocaleChange(profileData.locale);
handleLocaleChange(profileStore.locale);
const orgRes = await getOrganizations(profileData.id, isOrgSite, orgSiteName); const orgRes = await getOrganizations(profileData.id, isOrgSite, orgSiteName);
@@ -1,6 +1,6 @@
import { LOCALE } from '$lib/utils/types';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import i18n, { type Config } from 'sveltekit-i18n'; import i18n, { type Config } from 'sveltekit-i18n';
import { LOCALE } from '$lib/utils/types';
interface Params { interface Params {
dateValue: number; dateValue: number;
@@ -10,8 +10,7 @@ interface Params {
val: Date; val: Date;
} }
const config: Config<Partial<Params>> = { export const config: Config<Partial<Params>> = {
initLocale: 'en',
// parser: parser(), // parser: parser(),
loaders: [ loaders: [
{ {
@@ -1,7 +1,7 @@
import { t } from '$lib/utils/functions/translations';
import isNumber from 'lodash/isNumber'; import isNumber from 'lodash/isNumber';
import z from 'zod'; import z from 'zod';
import { validateEmail } from './validateEmail'; import { validateEmail } from './validateEmail';
import { t } from '$lib/utils/functions/translations';
const getOrgNameValidation = () => const getOrgNameValidation = () =>
z z
@@ -85,12 +85,12 @@ const forgotValidationSchema = z.object({
const authValidationSchema = z.object({ const authValidationSchema = z.object({
email: z.string().email({ email: z.string().email({
message: `${t.get('validations.auth.email.invalid_email')}`, message: 'validations.auth.email.invalid_email',
invalid_type_error: `${t.get('validations.invalid_type_error')}` invalid_type_error: 'validations.invalid_type_error'
}), }),
password: z.string().min(6, { password: z.string().min(6, {
message: `${t.get('validations.auth.password.min_char')}`, message: 'validations.auth.password.min_char',
invalid_type_error: `${t.get('validations.invalid_type_error')}` invalid_type_error: 'validations.invalid_type_error'
}) })
}); });
@@ -1,21 +1,21 @@
import { get } from 'svelte/store';
import { supabase } from '$lib/utils/functions/supabase';
import { isUUID } from '$lib/utils/functions/isUUID';
import { QUESTION_TYPE } from '$lib/components/Question/constants'; import { QUESTION_TYPE } from '$lib/components/Question/constants';
import { STATUS } from '$lib/utils/constants/course';
import { isUUID } from '$lib/utils/functions/isUUID';
import { supabase } from '$lib/utils/functions/supabase';
import { isOrgAdmin } from '$lib/utils/store/org';
import type { import type {
Lesson,
Course, Course,
Exercise,
ExerciseTemplate,
Group, Group,
Groupmember, Groupmember,
Exercise, Lesson,
LessonCompletion, LessonCompletion,
ExerciseTemplate, LessonSection,
LessonSection ProfileCourseProgress
} from '$lib/utils/types'; } from '$lib/utils/types';
import { STATUS } from '$lib/utils/constants/course'; import type { PostgrestError, PostgrestSingleResponse } from '@supabase/supabase-js';
import type { PostgrestSingleResponse, PostgrestError } from '@supabase/supabase-js'; import { get } from 'svelte/store';
import type { ProfileCourseProgress } from '$lib/utils/types';
import { isOrgAdmin } from '$lib/utils/store/org';
export async function fetchCourses(profileId, orgId) { export async function fetchCourses(profileId, orgId) {
if (!orgId || !profileId) return; if (!orgId || !profileId) return;
@@ -144,10 +144,7 @@ export async function fetchCourse(courseId?: Course['id'], slug?: Course['slug']
const { data, error } = response; const { data, error } = response;
console.log(`error`, error);
console.log(`data`, data);
if (!data || error) { if (!data || error) {
console.log(`data`, data);
console.log(`fetchCourse => error`, error); console.log(`fetchCourse => error`, error);
// return this.redirect(307, '/courses'); // return this.redirect(307, '/courses');
return { data, error }; return { data, error };
@@ -1,11 +1,11 @@
import { get } from 'svelte/store';
import type { PostgrestError } from '@supabase/supabase-js';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { supabase } from '$lib/utils/functions/supabase';
import { orgs, currentOrg, orgAudience, orgTeam } from '$lib/utils/store/org';
import { ROLE, ROLE_LABEL } from '$lib/utils/constants/roles'; import { ROLE, ROLE_LABEL } from '$lib/utils/constants/roles';
import type { CurrentOrg, OrgTeamMember } from '$lib/utils/types/org'; import { supabase } from '$lib/utils/functions/supabase';
import { currentOrg, orgAudience, orgs, orgTeam } from '$lib/utils/store/org';
import type { OrganizationPlan } from '$lib/utils/types'; import type { OrganizationPlan } from '$lib/utils/types';
import type { CurrentOrg, OrgTeamMember } from '$lib/utils/types/org';
import type { PostgrestError } from '@supabase/supabase-js';
import { get } from 'svelte/store';
export async function getOrgTeam(orgId: string) { export async function getOrgTeam(orgId: string) {
const { data, error } = await supabase const { data, error } = await supabase
@@ -164,8 +164,6 @@ export async function getOrgAudience(orgId: string) {
.eq('groupmember.group.organization_id', orgId) .eq('groupmember.group.organization_id', orgId)
.eq('groupmember.role_id', ROLE.STUDENT); .eq('groupmember.role_id', ROLE.STUDENT);
console.log('data', data);
const audience = (data || []).map((profile) => ({ const audience = (data || []).map((profile) => ({
id: profile.id, id: profile.id,
name: profile.fullname, name: profile.fullname,
@@ -1293,7 +1293,29 @@
"email": "Deine E-Mail", "email": "Deine E-Mail",
"password": "Ihr Passwort", "password": "Ihr Passwort",
"forgot": "Passwort vergessen?", "forgot": "Passwort vergessen?",
"login": "Anmeldung" "login": "Anmeldung",
"login_with": "oder loggen Sie sich mit ein",
"signup_with": "oder melden Sie sich an",
"already_have_account": "Sie haben bereits ein Konto?",
"not_registered_yet": "Noch nicht registriert?",
"signup": "Melden Sie sich an",
"signup_with_google": "Melden Sie sich bei Google an",
"login_with_google": "Melden Sie sich mit Google an",
"create_to_join": "Erstellen Sie ein kostenloses Konto, um Mitglied zu werden",
"login_to_join": "Melden Sie sich an, um beizutreten",
"validation": {
"unable_to_create_profile": "Profil kann nicht erstellt werden",
"user_cannot_be_created": "Benutzer kann nicht erstellt werden"
},
"fields": {
"email": "Ihre E-Mail",
"full_name": "Vollständiger Name",
"password": "Ihr Passwort",
"confirm_password": "Passwort bestätigen",
"password_helper_message": "Das Passwort muss mehr als 6 Zeichen lang sein",
"create_account": "Benutzerkonto erstellen"
},
"create_account": "Benutzerkonto erstellen"
}, },
"welcome_modal": { "welcome_modal": {
"we_at": "Wir bei", "we_at": "Wir bei",
@@ -49,7 +49,29 @@
"email": "Your email", "email": "Your email",
"password": "Your password", "password": "Your password",
"forgot": "Forgot password?", "forgot": "Forgot password?",
"login": "Log In" "login": "Log In",
"login_with": "or login with",
"signup_with": "or sign up with",
"already_have_account": "Already have an account?",
"not_registered_yet": "Not registered yet?",
"signup": "Sign up",
"signup_with_google": "Sign up with Google",
"login_with_google": "Login with Google",
"create_to_join": "Create a free account to join",
"create_account": "Create Account",
"login_to_join": "Log in to join",
"fields": {
"email": "Your Email",
"full_name": "Full Name",
"password": "Your Password",
"confirm_password": "Confirm Password",
"password_helper_message": "Password must be more than 6 characters",
"create_account": "Create Account"
},
"validation": {
"unable_to_create_profile": "Unable to create profile",
"user_cannot_be_created": "User cannot be created"
}
}, },
"profileMenu": { "profileMenu": {
"profile": "PROFILE", "profile": "PROFILE",
@@ -1293,7 +1293,29 @@
"email": "Tu correo electrónico", "email": "Tu correo electrónico",
"password": "Tu contraseña", "password": "Tu contraseña",
"forgot": "¿Has olvidado tu contraseña?", "forgot": "¿Has olvidado tu contraseña?",
"login": "Acceso" "login": "Acceso",
"login_with": "o inicia sesión con",
"signup_with": "o regístrate con",
"already_have_account": "¿Ya tienes una cuenta?",
"not_registered_yet": "¿Aún no estás registrado?",
"signup": "Inscribirse",
"signup_with_google": "Regístrate con Google",
"login_with_google": "Iniciar sesión con Google",
"create_to_join": "Crea una cuenta gratuita para unirte",
"login_to_join": "Inicia sesión para unirte",
"validation": {
"unable_to_create_profile": "No se puede crear el perfil",
"user_cannot_be_created": "No se puede crear usuario"
},
"fields": {
"email": "Tu correo electrónico",
"full_name": "Nombre completo",
"password": "Tu contraseña",
"confirm_password": "confirmar Contraseña",
"password_helper_message": "La contraseña debe tener más de 6 caracteres.",
"create_account": "Crear una cuenta"
},
"create_account": "Crear una cuenta"
}, },
"welcome_modal": { "welcome_modal": {
"we_at": "nosotros en", "we_at": "nosotros en",
@@ -1293,7 +1293,29 @@
"email": "Votre email", "email": "Votre email",
"password": "Votre mot de passe", "password": "Votre mot de passe",
"forgot": "Mot de passe oublié?", "forgot": "Mot de passe oublié?",
"login": "Se connecter" "login": "Se connecter",
"login_with": "ou connectez-vous avec",
"signup_with": "ou inscrivez-vous avec",
"already_have_account": "Vous avez déjà un compte ?",
"not_registered_yet": "Pas encore inscrit ?",
"signup": "S'inscrire",
"signup_with_google": "Inscrivez-vous avec Google",
"login_with_google": "Connectez-vous avec Google",
"create_to_join": "Créez un compte gratuit pour vous inscrire",
"login_to_join": "Connectez-vous pour rejoindre",
"validation": {
"unable_to_create_profile": "Impossible de créer un profil",
"user_cannot_be_created": "L'utilisateur ne peut pas être créé"
},
"fields": {
"email": "Votre e-mail",
"full_name": "Nom et prénom",
"password": "Votre mot de passe",
"confirm_password": "Confirmez le mot de passe",
"password_helper_message": "Le mot de passe doit comporter plus de 6 caractères",
"create_account": "Créer un compte"
},
"create_account": "Créer un compte"
}, },
"welcome_modal": { "welcome_modal": {
"we_at": "Nous à", "we_at": "Nous à",
@@ -1293,7 +1293,29 @@
"email": "आपका ईमेल", "email": "आपका ईमेल",
"password": "आपका पासवर्ड", "password": "आपका पासवर्ड",
"forgot": "पासवर्ड भूल गए?", "forgot": "पासवर्ड भूल गए?",
"login": "लॉग इन करें" "login": "लॉग इन करें",
"login_with": "या के साथ लॉगिन करें",
"signup_with": "या इसके साथ साइन अप करें",
"already_have_account": "क्या आपके पास पहले से एक खाता मौजूद है?",
"not_registered_yet": "अभी तक पंजीकृत नहीं हैं?",
"signup": "साइन अप करें",
"signup_with_google": "Google के साथ साइन अप करें",
"login_with_google": "गूगल से लॉगिन करें",
"create_to_join": "शामिल होने के लिए एक निःशुल्क खाता बनाएँ",
"login_to_join": "शामिल होने के लिए लॉग इन करें",
"validation": {
"unable_to_create_profile": "प्रोफ़ाइल बनाने में असमर्थ",
"user_cannot_be_created": "उपयोगकर्ता नहीं बनाया जा सकता"
},
"fields": {
"email": "आपका ईमेल",
"full_name": "पूरा नाम",
"password": "आपका पासवर्ड",
"confirm_password": "पासवर्ड की पुष्टि कीजिये",
"password_helper_message": "पासवर्ड 6 अक्षर से अधिक होना चाहिए",
"create_account": "खाता बनाएं"
},
"create_account": "खाता बनाएं"
}, },
"welcome_modal": { "welcome_modal": {
"we_at": "हम पर", "we_at": "हम पर",
@@ -1293,7 +1293,29 @@
"email": "Seu email", "email": "Seu email",
"password": "Sua senha", "password": "Sua senha",
"forgot": "Esqueceu sua senha?", "forgot": "Esqueceu sua senha?",
"login": "Conecte-se" "login": "Conecte-se",
"login_with": "ou faça login com",
"signup_with": "ou cadastre-se com",
"already_have_account": "Já tem uma conta?",
"not_registered_yet": "Ainda não está cadastrado?",
"signup": "Inscrever-se",
"signup_with_google": "Inscreva-se no Google",
"login_with_google": "Faça login com o Google",
"create_to_join": "Crie uma conta gratuita para participar",
"login_to_join": "Faça login para participar",
"validation": {
"unable_to_create_profile": "Não foi possível criar o perfil",
"user_cannot_be_created": "O usuário não pode ser criado"
},
"fields": {
"email": "Seu e-mail",
"full_name": "Nome completo",
"password": "Sua senha",
"confirm_password": "Confirme sua senha",
"password_helper_message": "A senha deve ter mais de 6 caracteres",
"create_account": "Criar uma conta"
},
"create_account": "Criar uma conta"
}, },
"welcome_modal": { "welcome_modal": {
"we_at": "Nós em", "we_at": "Nós em",
@@ -1293,7 +1293,29 @@
"email": "Ваш адрес электронной почты", "email": "Ваш адрес электронной почты",
"password": "Ваш пароль", "password": "Ваш пароль",
"forgot": "Забыли пароль?", "forgot": "Забыли пароль?",
"login": "Авторизоваться" "login": "Авторизоваться",
"login_with": "или войдите с помощью",
"signup_with": "или зарегистрируйтесь через",
"already_have_account": "У вас уже есть аккаунт?",
"not_registered_yet": "Еще не зарегистрированы?",
"signup": "Зарегистрироваться",
"signup_with_google": "Зарегистрируйтесь через Google",
"login_with_google": "Войти через Google",
"create_to_join": "Создайте бесплатную учетную запись, чтобы присоединиться",
"login_to_join": "Войдите, чтобы присоединиться",
"validation": {
"unable_to_create_profile": "Невозможно создать профиль",
"user_cannot_be_created": "Пользователь не может быть создан"
},
"fields": {
"email": "Ваш адрес электронной почты",
"full_name": "Полное имя",
"password": "Ваш пароль",
"confirm_password": "Подтвердите пароль",
"password_helper_message": "Пароль должен быть более 6 символов",
"create_account": "Зарегистрироваться"
},
"create_account": "Зарегистрироваться"
}, },
"welcome_modal": { "welcome_modal": {
"we_at": "Мы в", "we_at": "Мы в",
@@ -1293,7 +1293,29 @@
"email": "Email của bạn", "email": "Email của bạn",
"password": "Mật khẩu của bạn", "password": "Mật khẩu của bạn",
"forgot": "Quên mật khẩu?", "forgot": "Quên mật khẩu?",
"login": "Đăng nhập" "login": "Đăng nhập",
"login_with": "hoặc đăng nhập với",
"signup_with": "hoặc đăng ký với",
"already_have_account": "Đã có tài khoản?",
"not_registered_yet": "Chưa đăng ký?",
"signup": "Đăng ký",
"signup_with_google": "Đăng ký với Google",
"login_with_google": "Đăng nhập bằng Google",
"create_to_join": "Tạo một tài khoản miễn phí để tham gia",
"login_to_join": "Đăng nhập để tham gia",
"validation": {
"unable_to_create_profile": "Không thể tạo hồ sơ",
"user_cannot_be_created": "Không thể tạo người dùng"
},
"fields": {
"email": "Email của bạn",
"full_name": "Tên đầy đủ",
"password": "Mật khẩu của bạn",
"confirm_password": "Xác nhận mật khẩu",
"password_helper_message": "Mật khẩu phải nhiều hơn 6 ký tự",
"create_account": "Tạo tài khoản"
},
"create_account": "Tạo tài khoản"
}, },
"welcome_modal": { "welcome_modal": {
"we_at": "Chúng tôi ở", "we_at": "Chúng tôi ở",
+12 -29
View File
@@ -1,13 +1,12 @@
import type { MetaTagsProps } from 'svelte-meta-tags'; import { dev } from '$app/environment';
import { dev, browser } from '$app/environment';
import { redirect } from '@sveltejs/kit';
import { blockedSubdomain } from '$lib/utils/constants/app';
import { getCurrentOrg } from '$lib/utils/services/org';
import { getSupabase, supabase } from '$lib/utils/functions/supabase';
import { loadTranslations } from '$lib/utils/functions/translations';
import type { CurrentOrg } from '$lib/utils/types/org';
import { IS_SELFHOSTED } from '$env/static/private';
import { env } from '$env/dynamic/private'; import { env } from '$env/dynamic/private';
import { IS_SELFHOSTED } from '$env/static/private';
import { blockedSubdomain } from '$lib/utils/constants/app';
import { getSupabase, supabase } from '$lib/utils/functions/supabase';
import { getCurrentOrg } from '$lib/utils/services/org';
import type { CurrentOrg } from '$lib/utils/types/org';
import { redirect } from '@sveltejs/kit';
import type { MetaTagsProps } from 'svelte-meta-tags';
if (!supabase) { if (!supabase) {
getSupabase(); getSupabase();
@@ -19,19 +18,19 @@ interface LoadOutput {
skipAuth: boolean; skipAuth: boolean;
org: CurrentOrg | null; org: CurrentOrg | null;
baseMetaTags: MetaTagsProps; baseMetaTags: MetaTagsProps;
serverLang: string;
} }
export const load = async ({ url, cookies }): Promise<LoadOutput> => { export const load = async ({ url, cookies, request }): Promise<LoadOutput> => {
const response: LoadOutput = { const response: LoadOutput = {
orgSiteName: '', orgSiteName: '',
isOrgSite: false, isOrgSite: false,
skipAuth: false, skipAuth: false,
org: null, org: null,
baseMetaTags: getBaseMetaTags(url) baseMetaTags: getBaseMetaTags(url),
serverLang: request.headers?.get('accept-language') || ''
}; };
console.log('IS_SELFHOSTED', IS_SELFHOSTED);
// Selfhosted usecase would be here // Selfhosted usecase would be here
if (IS_SELFHOSTED === 'true') { if (IS_SELFHOSTED === 'true') {
const subdomain = getSubdomain(url); const subdomain = getSubdomain(url);
@@ -105,11 +104,6 @@ export const load = async ({ url, cookies }): Promise<LoadOutput> => {
throw redirect(307, 'https://app.classroomio.com'); throw redirect(307, 'https://app.classroomio.com');
} }
// Load translations
const { pathname } = url;
const initLocale = getInitialLocale();
await loadTranslations(initLocale, pathname);
return response; return response;
}; };
@@ -151,18 +145,7 @@ function getBaseMetaTags(url: URL) {
}); });
} }
// Define getInitialLocale function
function getInitialLocale(): string {
if (browser) {
try {
return window.navigator.language.split('-')[0];
} catch (e) {
return 'en';
}
}
return 'en';
}
function getSubdomain(url: URL) { function getSubdomain(url: URL) {
const host = url.host.replace('www.', ''); const host = url.host.replace('www.', '');
+7 -6
View File
@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { page } from '$app/stores'; import { page } from '$app/stores';
import debounce from 'lodash/debounce';
import Apps from '$lib/components/Apps/index.svelte'; import Apps from '$lib/components/Apps/index.svelte';
import { course } from '$lib/components/Course/store'; import { course } from '$lib/components/Course/store';
import OrgNavigation from '$lib/components/Navigation/app.svelte'; import OrgNavigation from '$lib/components/Navigation/app.svelte';
@@ -22,7 +24,6 @@
import { globalStore } from '$lib/utils/store/app'; import { globalStore } from '$lib/utils/store/app';
import { currentOrg } from '$lib/utils/store/org'; import { currentOrg } from '$lib/utils/store/org';
import { isMobile } from '$lib/utils/store/useMobile'; import { isMobile } from '$lib/utils/store/useMobile';
import { user } from '$lib/utils/store/user';
import { Theme } from 'carbon-components-svelte'; import { Theme } from 'carbon-components-svelte';
import type { CarbonTheme } from 'carbon-components-svelte/types/Theme/Theme.svelte'; import type { CarbonTheme } from 'carbon-components-svelte/types/Theme/Theme.svelte';
import merge from 'lodash/merge'; import merge from 'lodash/merge';
@@ -42,6 +43,8 @@
isMobile.update(() => window.innerWidth <= 760); isMobile.update(() => window.innerWidth <= 760);
} }
const getProfileDebounced = debounce(getProfile, 1000);
onMount(() => { onMount(() => {
console.log( console.log(
'Welcome to ClassroomIO, we are grateful you chose us.', 'Welcome to ClassroomIO, we are grateful you chose us.',
@@ -77,18 +80,15 @@
} }
// Skip Authentication // Skip Authentication
if (data.skipAuth || $user.fetchingUser) return; if (data.skipAuth) return;
// Authentication Steps // Authentication Steps
if (event === 'SIGNED_IN' || event === 'INITIAL_SESSION') { if (event === 'SIGNED_IN' || event === 'INITIAL_SESSION') {
$user.fetchingUser = true; getProfileDebounced({
getProfile({
path, path,
queryParam, queryParam,
isOrgSite: data.isOrgSite, isOrgSite: data.isOrgSite,
orgSiteName: data.orgSiteName orgSiteName: data.orgSiteName
}).then(() => {
$user.fetchingUser = false;
}); });
} }
// else if (!['TOKEN_REFRESHED'].includes(event)) { // else if (!['TOKEN_REFRESHED'].includes(event)) {
@@ -125,6 +125,7 @@
<Theme bind:theme={carbonTheme} /> <Theme bind:theme={carbonTheme} />
<UpgradeModal /> <UpgradeModal />
<Snackbar /> <Snackbar />
{#if data.org?.is_restricted || $currentOrg.is_restricted} {#if data.org?.is_restricted || $currentOrg.is_restricted}
+28
View File
@@ -0,0 +1,28 @@
import { config, loadTranslations } from '$lib/utils/functions/translations';
import { profile } from '$lib/utils/store/user';
import { get } from 'svelte/store';
const SUPPORTED_LANGUAGES = config?.loaders?.map((loader) => loader.locale) || [];
export const load = async ({ url, data }) => {
const { pathname } = url;
const profileStore = get(profile);
const serverLang = data.serverLang.split('-')[0];
const userLocale = profileStore.id ? profileStore.locale : getInitialLocale(serverLang);
const initLocale = getInitialLocale(userLocale);
await loadTranslations(initLocale, pathname); // keep this just before the `return`
return data;
};
function getInitialLocale(lang: string): string {
const locale = lang.split('-')[0];
if (SUPPORTED_LANGUAGES.includes(locale)) return locale;
return 'en';
}
@@ -1,21 +1,22 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import AuthUI from '$lib/components/AuthUI/index.svelte';
import type { Profile } from '$lib/components/Course/components/People/types';
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { SIGNUP_FIELDS } from '$lib/utils/constants/authentication';
import { logout } from '$lib/utils/functions/logout';
import { getSupabase } from '$lib/utils/functions/supabase'; import { getSupabase } from '$lib/utils/functions/supabase';
import { setTheme } from '$lib/utils/functions/theme';
import { t } from '$lib/utils/functions/translations';
import { import {
authValidation, authValidation,
getConfirmPasswordError, getConfirmPasswordError,
getDisableSubmit getDisableSubmit
} from '$lib/utils/functions/validator'; } from '$lib/utils/functions/validator';
import { SIGNUP_FIELDS } from '$lib/utils/constants/authentication';
import AuthUI from '$lib/components/AuthUI/index.svelte';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org'; import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
import { setTheme } from '$lib/utils/functions/theme';
import type { CurrentOrg } from '$lib/utils/types/org'; import type { CurrentOrg } from '$lib/utils/types/org';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { Profile } from '$lib/components/Course/components/People/types';
import { logout } from '$lib/utils/functions/logout';
export let data; export let data;
@@ -61,7 +62,7 @@
if (error) throw error; if (error) throw error;
if (!signupData.user) { if (!signupData.user) {
throw 'User cannot be created '; throw $t('login.validation.user_cannot_be_created');
} }
// Insert profile // Insert profile
@@ -86,7 +87,7 @@
} }
if (!profile?.id) { if (!profile?.id) {
throw 'Unable to create profile'; throw $t('login.validation.unable_to_create_profile');
} }
const res = await supabase.auth.signInWithPassword({ const res = await supabase.auth.signInWithPassword({
@@ -142,9 +143,6 @@
$: disableSubmit = getDisableSubmit(fields); $: disableSubmit = getDisableSubmit(fields);
$: console.log('data', data.invite); $: console.log('data', data.invite);
$: console.log('org', $currentOrg);
$: console.log('path', $currentOrgPath);
</script> </script>
<svelte:head> <svelte:head>
@@ -153,15 +151,15 @@
<AuthUI {supabase} isLogin={false} {handleSubmit} isLoading={loading} showLogo={true} bind:formRef> <AuthUI {supabase} isLogin={false} {handleSubmit} isLoading={loading} showLogo={true} bind:formRef>
<div class="mt-4 w-full"> <div class="mt-4 w-full">
<p class="dark:text-white text-lg font-semibold mb-6"> <p class="mb-6 text-lg font-semibold dark:text-white">
{#if data.invite.profile} {#if data.invite.profile}
Log in to join {$t('login.login_to_join')}
{:else} {:else}
Create a free account to join {$t('login.create_to_join')}
{/if} {/if}
</p> </p>
<TextField <TextField
label="Your Email" label={$t('login.fields.email')}
value={data.invite.email} value={data.invite.email}
type="email" type="email"
placeholder="you@domain.com" placeholder="you@domain.com"
@@ -171,7 +169,7 @@
/> />
{#if !data.invite.profile} {#if !data.invite.profile}
<TextField <TextField
label="Full Name" label={$t('login.fields.full_name')}
bind:value={fields.name} bind:value={fields.name}
type="text" type="text"
autoFocus={true} autoFocus={true}
@@ -184,7 +182,7 @@
/> />
{/if} {/if}
<TextField <TextField
label="Your Password" label={$t('login.fields.password')}
bind:value={fields.password} bind:value={fields.password}
type="password" type="password"
placeholder="************" placeholder="************"
@@ -192,12 +190,12 @@
inputClassName="w-full" inputClassName="w-full"
isDisabled={loading} isDisabled={loading}
errorMessage={errors.password} errorMessage={errors.password}
helperMessage="Password must be more than 6 characters" helperMessage={$t('login.fields.password_helper_message')}
isRequired isRequired
/> />
{#if !data.invite.profile} {#if !data.invite.profile}
<TextField <TextField
label="Confirm Password" label={$t('login.fields.confirm_password')}
bind:value={fields.confirmPassword} bind:value={fields.confirmPassword}
type="password" type="password"
placeholder="************" placeholder="************"
@@ -213,7 +211,7 @@
{/if} {/if}
</div> </div>
<div class="my-4 w-full flex justify-end items-center"> <div class="my-4 flex w-full items-center justify-end">
<PrimaryButton <PrimaryButton
label="Accept Invite" label="Accept Invite"
type="submit" type="submit"
+10 -14
View File
@@ -1,15 +1,14 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import AuthUI from '$lib/components/AuthUI/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { getSupabase } from '$lib/utils/functions/supabase';
import { authValidation } from '$lib/utils/functions/validator';
import { LOGIN_FIELDS } from '$lib/utils/constants/authentication'; import { LOGIN_FIELDS } from '$lib/utils/constants/authentication';
import AuthUI from '$lib/components/AuthUI/index.svelte'; import { getSupabase } from '$lib/utils/functions/supabase';
import { currentOrg } from '$lib/utils/store/org';
import { capturePosthogEvent } from '$lib/utils/services/posthog';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
import { authValidation } from '$lib/utils/functions/validator';
import { capturePosthogEvent } from '$lib/utils/services/posthog';
import { globalStore } from '$lib/utils/store/app'; import { globalStore } from '$lib/utils/store/app';
import { currentOrg } from '$lib/utils/store/org';
let formRef: HTMLFormElement; let formRef: HTMLFormElement;
let supabase = getSupabase(); let supabase = getSupabase();
@@ -18,9 +17,6 @@
let loading = false; let loading = false;
let errors = Object.assign({}, LOGIN_FIELDS); let errors = Object.assign({}, LOGIN_FIELDS);
let query = new URLSearchParams($page.url.search);
let redirect = query.get('redirect');
async function handleSubmit() { async function handleSubmit() {
const validationRes = authValidation(fields); const validationRes = authValidation(fields);
console.log('validationRes', validationRes); console.log('validationRes', validationRes);
@@ -48,7 +44,7 @@
email: fields.email email: fields.email
}); });
} }
} catch (error) { } catch (error: any) {
submitError = error.error_description || error.message; submitError = error.error_description || error.message;
loading = false; loading = false;
} }
@@ -61,7 +57,7 @@
<AuthUI {supabase} isLogin={true} {handleSubmit} isLoading={loading} bind:formRef> <AuthUI {supabase} isLogin={true} {handleSubmit} isLoading={loading} bind:formRef>
<div class="mt-4 w-full"> <div class="mt-4 w-full">
<p class="dark:text-white text-lg font-semibold mb-6">{$t('login.welcome')}</p> <p class="mb-6 text-lg font-semibold dark:text-white">{$t('login.welcome')}</p>
<TextField <TextField
label={$t('login.email')} label={$t('login.email')}
bind:value={fields.email} bind:value={fields.email}
@@ -71,7 +67,7 @@
className="mb-6" className="mb-6"
inputClassName="w-full" inputClassName="w-full"
isDisabled={loading} isDisabled={loading}
errorMessage={errors.email} errorMessage={$t(errors.email)}
/> />
<TextField <TextField
label={$t('login.password')} label={$t('login.password')}
@@ -81,7 +77,7 @@
className="mb-6" className="mb-6"
inputClassName="w-full" inputClassName="w-full"
isDisabled={loading} isDisabled={loading}
errorMessage={errors.password} errorMessage={$t(errors.password)}
/> />
{#if submitError} {#if submitError}
<p class="text-sm text-red-500">{submitError}</p> <p class="text-sm text-red-500">{submitError}</p>
@@ -91,7 +87,7 @@
</div> </div>
</div> </div>
<div class="my-4 w-full flex justify-end items-center"> <div class="my-4 flex w-full items-center justify-end">
<!-- <a href="/login" class="text-primary-700 text-sm">Create an account</a> --> <!-- <a href="/login" class="text-primary-700 text-sm">Create an account</a> -->
<PrimaryButton <PrimaryButton
label={$t('login.login')} label={$t('login.login')}
@@ -9,6 +9,23 @@ async function getGenericData(orgSlug: string): Promise<{
orgHasAvatarUrl: boolean; orgHasAvatarUrl: boolean;
courseData: any; courseData: any;
}> { }> {
const { data: publishedCourse } = await supabase
.from('course')
.select(
`
is_published,
group:group_id!inner(
organization!inner(
siteName
)
)
`
)
.eq('is_published', true)
.eq('group.organization.siteName', orgSlug)
.limit(1)
.returns<{ is_published: boolean }>();
const { data } = await supabase const { data } = await supabase
.from('course') .from('course')
.select( .select(
@@ -23,14 +40,13 @@ async function getGenericData(orgSlug: string): Promise<{
) )
` `
) )
.eq('group.organization.siteName', orgSlug); .eq('group.organization.siteName', orgSlug)
.limit(1);
const result = data || []; const result = data || [];
console.log('courseData', result);
return { return {
isCoursePublished: result.some((c) => c.is_published === true), isCoursePublished: publishedCourse?.is_published === true,
isCourseCreated: result.length > 0, isCourseCreated: result.length > 0,
orgHasAvatarUrl: !!get(result[0], 'group.organization.avatar_url', ''), orgHasAvatarUrl: !!get(result[0], 'group.organization.avatar_url', ''),
courseData: result courseData: result
@@ -54,7 +70,8 @@ async function getIsLessonCreated(
) )
` `
) )
.eq('course.group.organization.siteName', orgSlug); .eq('course.group.organization.siteName', orgSlug)
.limit(1);
console.log('lessonsData', data); console.log('lessonsData', data);
return { return {
@@ -80,7 +97,8 @@ async function getIsExerciseCreated(orgSlug: string): Promise<boolean> {
) )
` `
) )
.eq('lesson.course.group.organization.siteName', orgSlug); .eq('lesson.course.group.organization.siteName', orgSlug)
.limit(1);
return Array.isArray(data) ? data?.length > 0 : false; return Array.isArray(data) ? data?.length > 0 : false;
} }
+16 -15
View File
@@ -1,21 +1,22 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import AuthUI from '$lib/components/AuthUI/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import SenjaEmbed from '$lib/components/Senja/Embed.svelte';
import { SIGNUP_FIELDS } from '$lib/utils/constants/authentication';
import { getSupabase } from '$lib/utils/functions/supabase'; import { getSupabase } from '$lib/utils/functions/supabase';
import { t } from '$lib/utils/functions/translations';
import { import {
authValidation, authValidation,
getConfirmPasswordError, getConfirmPasswordError,
getDisableSubmit getDisableSubmit
} from '$lib/utils/functions/validator'; } from '$lib/utils/functions/validator';
import { SIGNUP_FIELDS } from '$lib/utils/constants/authentication';
import AuthUI from '$lib/components/AuthUI/index.svelte';
import { profile } from '$lib/utils/store/user';
import { currentOrg } from '$lib/utils/store/org';
import { capturePosthogEvent } from '$lib/utils/services/posthog'; import { capturePosthogEvent } from '$lib/utils/services/posthog';
import { globalStore } from '$lib/utils/store/app'; import { globalStore } from '$lib/utils/store/app';
import SenjaEmbed from '$lib/components/Senja/Embed.svelte'; import { currentOrg } from '$lib/utils/store/org';
import { profile } from '$lib/utils/store/user';
let supabase = getSupabase(); let supabase = getSupabase();
let fields = Object.assign({}, SIGNUP_FIELDS); let fields = Object.assign({}, SIGNUP_FIELDS);
@@ -112,7 +113,7 @@
formRef?.reset(); formRef?.reset();
success = true; success = true;
fields = Object.assign({}, SIGNUP_FIELDS); fields = Object.assign({}, SIGNUP_FIELDS);
} catch (error) { } catch (error: any) {
submitError = error?.error_description || error?.message; submitError = error?.error_description || error?.message;
loading = false; loading = false;
} }
@@ -130,7 +131,7 @@
<AuthUI {supabase} isLogin={false} {handleSubmit} isLoading={loading} bind:formRef> <AuthUI {supabase} isLogin={false} {handleSubmit} isLoading={loading} bind:formRef>
<div class="mt-4 w-full"> <div class="mt-4 w-full">
<p class="dark:text-white text-lg font-semibold mb-6">Create a free account</p> <p class="mb-6 text-lg font-semibold dark:text-white">Create a free account</p>
<!-- <TextField <!-- <TextField
label="Full Name" label="Full Name"
bind:value={fields.name} bind:value={fields.name}
@@ -144,30 +145,30 @@
isRequired isRequired
/> --> /> -->
<TextField <TextField
label="Your Email" label={$t('login.fields.email')}
bind:value={fields.email} bind:value={fields.email}
type="email" type="email"
placeholder="you@domain.com" placeholder="you@domain.com"
className="mb-6" className="mb-6"
inputClassName="w-full" inputClassName="w-full"
isDisabled={loading} isDisabled={loading}
errorMessage={errors.email} errorMessage={$t(errors.email ?? '')}
isRequired isRequired
/> />
<TextField <TextField
label="Your Password" label={$t('login.fields.password')}
bind:value={fields.password} bind:value={fields.password}
type="password" type="password"
placeholder="************" placeholder="************"
className="mb-6" className="mb-6"
inputClassName="w-full" inputClassName="w-full"
isDisabled={loading} isDisabled={loading}
errorMessage={errors.password} errorMessage={$t(errors.password ?? '')}
helperMessage="Password must be more than 6 characters" helperMessage={$t('login.fields.password_helper_message')}
isRequired isRequired
/> />
<TextField <TextField
label="Confirm Password" label={$t('login.fields.confirm_password')}
bind:value={fields.confirmPassword} bind:value={fields.confirmPassword}
type="password" type="password"
placeholder="************" placeholder="************"
@@ -182,9 +183,9 @@
{/if} {/if}
</div> </div>
<div class="my-4 w-full flex justify-end items-center"> <div class="my-4 flex w-full items-center justify-end">
<PrimaryButton <PrimaryButton
label="Create Account" label={$t('login.create_account')}
type="submit" type="submit"
className="sm:w-full w-full" className="sm:w-full w-full"
isDisabled={disableSubmit || loading} isDisabled={disableSubmit || loading}