feat: multi language support (#254)

* feat: multitranslational support

* - completion of settings page
- completion of sidebar

* sidebar rerendering bug fix

* completion of the lesson navigation & materials page

* completion of the course navigation

* completion of requested languages

* - Implementation of Language Picker
- Rename Bug Fixes

* checkpoint save

* - addition of the language picker to the onboarding.
- addition of the language switcher for the lesson notes.

* merge completion

* initialization of the new branch

* add transition.svelte file

* lesson note bug fix

* feat: add translation migration

* completion of translation

* fix height issue of dropdown

* refactoring

* fix: improve code with LOCALE from db

* finally get it fully functional

* connect with AI

* backward compatibility for lesson note

* preserve old content, when generating AI content

* undo translations in upgrade modal

* fix duplicate key issue from codacy trigger

---------

Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Adeyemi Adetunji
2024-04-19 05:45:48 +01:00
committed by GitHub
co-authored by Rotimi Best
parent ff7d2cee35
commit d75e2f258e
153 changed files with 11264 additions and 1130 deletions
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
export let url: string; export let url: string;
</script> </script>
+1
View File
@@ -54,6 +54,7 @@
"stripe": "^14.22.0", "stripe": "^14.22.0",
"svelte-email": "^0.0.4", "svelte-email": "^0.0.4",
"svelte-ui": "workspace:*", "svelte-ui": "workspace:*",
"sveltekit-i18n": "^2.4.2",
"unsplash-js": "^7.0.18", "unsplash-js": "^7.0.18",
"wait-on": "^7.0.1", "wait-on": "^7.0.1",
"zod": "^3.21.4" "zod": "^3.21.4"
@@ -14,6 +14,7 @@
import { group } from '$lib/components/Course/store'; import { group } from '$lib/components/Course/store';
import { profile } from '$lib/utils/store/user'; import { profile } from '$lib/utils/store/user';
import { calDateDiff } from '$lib/utils/functions/date'; import { calDateDiff } from '$lib/utils/functions/date';
import { t } from '$lib/utils/functions/translations';
export let handleClose = () => {}; export let handleClose = () => {};
@@ -49,7 +50,7 @@
...comments, ...comments,
{ {
comment: comment, comment: comment,
name: 'You', name: $t('course.navItem.lessons.comments.you'),
avatar: $profile.avatar_url, avatar: $profile.avatar_url,
commentAt: new Date() commentAt: new Date()
} }
@@ -165,7 +166,7 @@
commentAt: lessonComment.created_at, commentAt: lessonComment.created_at,
name: name:
lessonComment.groupmember.id === groupmember?.id lessonComment.groupmember.id === groupmember?.id
? 'You' ? $t('course.navItem.lessons.comments.you')
: lessonComment.groupmember.profile.fullname : lessonComment.groupmember.profile.fullname
}; };
}); });
@@ -191,7 +192,7 @@
$: !isFetched && fetchComments($group.people, $lesson.id); $: !isFetched && fetchComments($group.people, $lesson.id);
</script> </script>
<PageNav title="Comments" overidableStyle="padding: 0 10px"> <PageNav title={$t('course.navItem.lessons.comments.title')} overidableStyle="padding: 0 10px">
<div slot="widget"> <div slot="widget">
<CloseButton onClick={handleClose} /> <CloseButton onClick={handleClose} />
</div> </div>
@@ -224,7 +225,7 @@
<div class="footer"> <div class="footer">
<TextField <TextField
placeholder="Say something" placeholder={$t('course.navItem.lessons.comments.placeholder')}
bind:value={comment} bind:value={comment}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
isDisabled={isSaving} isDisabled={isSaving}
@@ -8,6 +8,7 @@
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import { Add, TrashCan } from 'carbon-icons-svelte'; import { Add, TrashCan } from 'carbon-icons-svelte';
import IconButton from '$lib/components/IconButton/index.svelte'; import IconButton from '$lib/components/IconButton/index.svelte';
import { t } from '$lib/utils/functions/translations';
export let title = 'Poll'; export let title = 'Poll';
export let isSaving = false; export let isSaving = false;
@@ -20,7 +21,7 @@
question: '', question: '',
expiration: new Date().toDateString(), expiration: new Date().toDateString(),
isPublic: false, isPublic: false,
status: 'draft', status: $t('course.navItem.lessons.poll.draft'),
author: { author: {
id: $profile.id || '', id: $profile.id || '',
username: $profile.username || '', username: $profile.username || '',
@@ -70,14 +71,14 @@
</div> </div>
<div class="p-3"> <div class="p-3">
<TextField <TextField
label="Question" label={$t('course.navItem.lessons.poll.question')}
className="w-full mb-3" className="w-full mb-3"
bind:value={poll.question} bind:value={poll.question}
placeholder="Poll question" placeholder={$t('course.navItem.lessons.poll.poll_question')}
isRequired={true} isRequired={true}
/> />
<DateTime <DateTime
label="Expiration" label={$t('course.navItem.lessons.poll.expiration')}
className="w-full mb-3" className="w-full mb-3"
bind:value={poll.expiration} bind:value={poll.expiration}
isRequired={true} isRequired={true}
@@ -85,7 +86,7 @@
<div> <div>
<p class="dark:text-white p-0 m-0 mb-1 text-md flex items-center gap-2"> <p class="dark:text-white p-0 m-0 mb-1 text-md flex items-center gap-2">
Options {$t('course.navItem.lessons.poll.options')}
<IconButton onClick={handleAddOptions} contained={true} size="small"> <IconButton onClick={handleAddOptions} contained={true} size="small">
<Add size={16} /> <Add size={16} />
@@ -99,7 +100,7 @@
label="" label=""
className="w-full" className="w-full"
bind:value={option.label} bind:value={option.label}
placeholder="Option Label" placeholder={$t('course.navItem.lessons.poll.option_label')}
isRequired={true} isRequired={true}
/> />
<IconButton onClick={handleRemoveOptions(index)} contained={true} size="small"> <IconButton onClick={handleRemoveOptions(index)} contained={true} size="small">
@@ -111,11 +112,15 @@
<div class="w-full flex justify-center gap-2 mb-3"> <div class="w-full flex justify-center gap-2 mb-3">
<PrimaryButton <PrimaryButton
label="Create poll" label={$t('course.navItem.lessons.poll.create_poll')}
onClick={finishPoll} onClick={finishPoll}
isLoading={isSaving} isLoading={isSaving}
isDisabled={isCreateDisabled} isDisabled={isCreateDisabled}
/> />
<PrimaryButton label="Cancel" variant={VARIANTS.OUTLINED} onClick={onCancel} /> <PrimaryButton
label={$t('course.navItem.lessons.poll.cancel')}
variant={VARIANTS.OUTLINED}
onClick={onCancel}
/>
</div> </div>
</div> </div>
@@ -1,10 +1,12 @@
<script lang="ts"> <script script lang="ts">
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import Label from './Label.svelte'; import Label from './Label.svelte';
import Avatar from './Avatar.svelte'; import Avatar from './Avatar.svelte';
import type { PollType } from '../types'; import type { PollType } from '../types';
import { updatePollStatus } from '../service'; import { updatePollStatus } from '../service';
import { t } from '$lib/utils/functions/translations';
import { Dashboard } from 'carbon-icons-svelte';
export let poll: PollType; export let poll: PollType;
export let onSelect: (a: string) => void; export let onSelect: (a: string) => void;
@@ -62,7 +64,7 @@
{`<`} {`<`}
</button> </button>
<div class="flex-grow text-center"> <div class="flex-grow text-center">
<h3 class="dark:text-white font-bold">Voted</h3> <h3 class="dark:text-white font-bold">{$t('course.navItem.lessons.poll.voted')}</h3>
</div> </div>
</div> </div>
{#each selectedOptionToView.selectedBy as user} {#each selectedOptionToView.selectedBy as user}
@@ -81,13 +83,16 @@
{poll.question} {poll.question}
</h3> </h3>
<p class="dark:text-white text-sm italic"> <p class="dark:text-white text-sm italic">
Created by <span class="text-primary-500">@{poll.author.username}</span> {$t('course.navItem.lessons.poll.created_by')}
<span class="text-primary-500">@{poll.author.username}</span>
</p> </p>
</div> </div>
{#if isAuthor} {#if isAuthor}
<div class="flex mb-3 justify-end items-center"> <div class="flex mb-3 justify-end items-center">
<label class="mr-2 font-bold" for="user-select">Status:</label> <label class="mr-2 font-bold" for="user-select"
>{$t('course.navItem.lessons.poll.status')}:</label
>
<select <select
name="users" name="users"
@@ -95,8 +100,8 @@
class="dark:text-white dark:bg-black" class="dark:text-white dark:bg-black"
bind:value={poll.status} bind:value={poll.status}
> >
<option value="draft">Draft</option> <option value="draft">{$t('course.navItem.lessons.poll.draft')}</option>
<option value="published">Publish</option> <option value="published">{$t('course.navItem.lessons.poll.publish')}</option>
</select> </select>
</div> </div>
{/if} {/if}
@@ -105,20 +110,20 @@
{#if viewResult} {#if viewResult}
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="mr-5"> <div class="mr-5">
<Label>Expiration</Label> <Label>{$t('course.navItem.lessons.poll.expiration')}</Label>
<p class="dark:text-white text-sm text-gray-600 mb-4"> <p class="dark:text-white text-sm text-gray-600 mb-4">
{poll.expiration || 'Today'} {poll.expiration || $t('dashboard.today')}
</p> </p>
</div> </div>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<Label>Total Votes</Label> <Label>{$t('course.navItem.lessons.poll.total_votes')}</Label>
<p class="dark:text-white text-sm text-gray-600 mb-4"> <p class="dark:text-white text-sm text-gray-600 mb-4">
{totalVotes} {totalVotes}
</p> </p>
</div> </div>
</div> </div>
<Label>Responses</Label> <Label>{$t('course.navItem.lessons.poll.responses')}</Label>
{#each poll.options as option} {#each poll.options as option}
<div class="border-b p-5"> <div class="border-b p-5">
<p class="dark:text-white"> <p class="dark:text-white">
@@ -176,11 +181,11 @@
{/each} {/each}
<div class="flex items-center"> <div class="flex items-center">
<p class="dark:text-white text-sm text-gray-600"> <p class="dark:text-white text-sm text-gray-600">
Expires: {poll.expiration || 'Today'} {$t('course.navItem.lessons.poll.expires')}: {poll.expiration || $t('dashboard.today')}
</p> </p>
<span class="mx-2">|</span> <span class="mx-2">|</span>
<p class="dark:text-white text-sm text-gray-600"> <p class="dark:text-white text-sm text-gray-600">
Voted: {totalVotes} {$t('course.navItem.lessons.poll.voted')}: {totalVotes}
</p> </p>
</div> </div>
{/if} {/if}
@@ -188,10 +193,16 @@
{#if isAuthor} {#if isAuthor}
<div class="w-full flex items-center mt-3"> <div class="w-full flex items-center mt-3">
<PrimaryButton <PrimaryButton
label={viewResult ? 'Back' : 'Result'} label={viewResult
? $t('course.navItem.lessons.poll.back')
: $t('course.navItem.lessons.poll.result')}
onClick={() => (viewResult = !viewResult)} onClick={() => (viewResult = !viewResult)}
/> />
<PrimaryButton label="Delete" variant={VARIANTS.TEXT_DANGER} onClick={handlePollDelete} /> <PrimaryButton
label={$t('course.navItem.lessons.poll.delete')}
variant={VARIANTS.TEXT_DANGER}
onClick={handlePollDelete}
/>
</div> </div>
{/if} {/if}
{/if} {/if}
@@ -20,6 +20,7 @@
RealtimePostgresChangesPayload, RealtimePostgresChangesPayload,
PostgrestSingleResponse PostgrestSingleResponse
} from '@supabase/supabase-js'; } from '@supabase/supabase-js';
import { t } from '$lib/utils/functions/translations';
export let handleClose = () => {}; export let handleClose = () => {};
@@ -62,7 +63,7 @@
if (error || !data) { if (error || !data) {
console.log(error); console.log(error);
snackbar.error('An error occurred while creating poll'); snackbar.error('snackbar.poll.error.creating_poll');
return; return;
} }
@@ -78,7 +79,7 @@
if (appsPollOptionsError || !appsPollOptionsData) { if (appsPollOptionsError || !appsPollOptionsData) {
console.log({ appsPollOptionsError }); console.log({ appsPollOptionsError });
snackbar.error('An error occurred while creating poll'); snackbar.error('snackbar.poll.error.creating_poll');
return; return;
} }
@@ -229,12 +230,12 @@
); );
tabs = [ tabs = [
{ {
label: 'Active polls', label: $t('course.navItem.lessons.polls.active_polls'),
value: 0, value: 0,
number: activePolls.length number: activePolls.length
}, },
{ {
label: 'Expired polls', label: $t('course.navItem.lessons.polls.expired_polls'),
value: 1, value: 1,
number: expiredPolls.length number: expiredPolls.length
} }
@@ -242,7 +243,11 @@
} }
</script> </script>
<PageNav title="Poll" overidableStyle="padding: 0 10px" paddingClass="w-full"> <PageNav
title={$t('course.navItem.lessons.polls.title')}
overidableStyle="padding: 0 10px"
paddingClass="w-full"
>
<div slot="widget"> <div slot="widget">
<CloseButton onClick={handleClose} /> <CloseButton onClick={handleClose} />
</div> </div>
@@ -260,7 +265,7 @@
<Tabs {tabs} bind:selectedTab onCreate={() => (openCreatePollForm = !openCreatePollForm)} /> <Tabs {tabs} bind:selectedTab onCreate={() => (openCreatePollForm = !openCreatePollForm)} />
{#if isLoading} {#if isLoading}
Loading... {$t('course.navItem.lessons.polls.loading')}...
{:else} {:else}
{#each selectedTab === tabs[0].value ? activePolls : expiredPolls as poll} {#each selectedTab === tabs[0].value ? activePolls : expiredPolls as poll}
<Poll <Poll
@@ -273,7 +278,7 @@
<div <div
class="bg-gray-100 dark:bg-neutral-800 border rounded-md h-60 flex items-center justify-center" class="bg-gray-100 dark:bg-neutral-800 border rounded-md h-60 flex items-center justify-center"
> >
<h2 class="text-xl font-bold">No polls to display</h2> <h2 class="text-xl font-bold">{$t('course.navItem.lessons.polls.no_polls')}</h2>
</div> </div>
{/each} {/each}
{/if} {/if}
@@ -45,7 +45,7 @@ export const updatePollStatus = async (pollId: string, status: PollType['status'
if (error) { if (error) {
console.log(error); console.log(error);
snackbar.error('An error occurred while updating poll status'); snackbar.error('snackbar.poll.error.updating_poll');
return; return;
} }
}; };
@@ -64,7 +64,7 @@ export const togglePollSubmission = async (
}); });
if (error) { if (error) {
console.log(error); console.log(error);
snackbar.error('An error occurred while submitting poll'); snackbar.error('snackbar.poll.error.submitting_poll');
return; return;
} }
} else { } else {
@@ -76,7 +76,7 @@ export const togglePollSubmission = async (
if (error) { if (error) {
console.log(error); console.log(error);
snackbar.error('An error occurred while submitting poll'); snackbar.error('snackbar.poll.error.submitting_poll');
return; return;
} }
} }
@@ -101,7 +101,7 @@ export function handleVote(pollId: string, groupmemberId: string, author: PollTy
if (poll.id === pollId) { if (poll.id === pollId) {
// Prevent user from voting on a poll that the status is not published // Prevent user from voting on a poll that the status is not published
if (poll.status !== 'published') { if (poll.status !== 'published') {
snackbar.info('Poll is not published yet'); snackbar.info('snackbar.poll.info.not_published');
return poll; return poll;
} }
@@ -109,7 +109,7 @@ export function handleVote(pollId: string, groupmemberId: string, author: PollTy
if ( if (
poll.options.some((option) => isOptionSelectedByCurrentUser(option, groupmemberId)) poll.options.some((option) => isOptionSelectedByCurrentUser(option, groupmemberId))
) { ) {
snackbar.info('You can only vote once'); snackbar.info('snackbar.poll.info.vote_once');
return poll; return poll;
} }
@@ -1,7 +1,30 @@
<script lang="ts"> <script lang="ts">
import { logout } from '$lib/utils/functions/logout';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { supabase } from '$lib/utils/functions/supabase';
import { capturePosthogEvent } from '$lib/utils/services/posthog';
import { currentOrg, orgs, defaultCurrentOrgState } from '$lib/utils/store/org';
import { user, profile, defaultProfileState, defaultUserState } from '$lib/utils/store/user';
import { t } from '$lib/utils/functions/translations';
import posthog from 'posthog-js';
import { goto } from '$app/navigation';
async function logout() {
const { error } = await supabase.auth.signOut();
if (error) {
console.error('Error logging out: ', error);
}
currentOrg.set(defaultCurrentOrgState);
orgs.set([]);
user.set(defaultUserState);
profile.set(defaultProfileState);
capturePosthogEvent('user_logged_out');
posthog.reset();
goto('/login');
}
</script> </script>
<PrimaryButton variant={VARIANTS.OUTLINED} label="Log out" onClick={logout} /> <PrimaryButton variant={VARIANTS.OUTLINED} label={$t('settings.profile.logout')} onClick={logout} />
@@ -4,6 +4,7 @@
import ArrowUpRightIcon from '$lib/components/Icons/ArrowTopRight.svelte'; import ArrowUpRightIcon from '$lib/components/Icons/ArrowTopRight.svelte';
import { currentOrgDomain } from '$lib/utils/store/org'; import { currentOrgDomain } from '$lib/utils/store/org';
import { isMobile } from '$lib/utils/store/useMobile'; import { isMobile } from '$lib/utils/store/useMobile';
import { t } from '$lib/utils/functions/translations';
export let className = ''; export let className = '';
</script> </script>
@@ -11,7 +12,7 @@
<a href={$currentOrgDomain} target="_blank" class="{className} ml-2 hover:no-underline"> <a href={$currentOrgDomain} target="_blank" class="{className} ml-2 hover:no-underline">
<PrimaryButton variant={VARIANTS.CONTAINED_DARK}> <PrimaryButton variant={VARIANTS.CONTAINED_DARK}>
{#if !$isMobile} {#if !$isMobile}
View site {$t('settings.subheadings.view_site')}
{/if} {/if}
<ArrowUpRightIcon /> <ArrowUpRightIcon />
</PrimaryButton> </PrimaryButton>
@@ -1,5 +1,6 @@
<script> <script>
export let label = 'Coming soon'; import { t } from '$lib/utils/functions/translations';
export let label = $t('course.navItem.lessons.exercises.new_exercise_modal.coming_soon');
</script> </script>
<p <p
@@ -11,6 +11,7 @@
import { course } from '$lib/components/Course/store'; import { course } from '$lib/components/Course/store';
import { currentOrg, isFreePlan } from '$lib/utils/store/org'; import { currentOrg, isFreePlan } from '$lib/utils/store/org';
import { globalStore } from '$lib/utils/store/app'; import { globalStore } from '$lib/utils/store/app';
import { t } from '$lib/utils/functions/translations';
import FlashFilled from 'carbon-icons-svelte/lib/FlashFilled.svelte'; import FlashFilled from 'carbon-icons-svelte/lib/FlashFilled.svelte';
const studentNamePlaceholder = 'Name of student'; const studentNamePlaceholder = 'Name of student';
@@ -52,9 +53,11 @@
<div class="flex-1 flex flex-col lg:flex-row justify-between gap-3 w-full mb-3 h-4/5"> <div class="flex-1 flex flex-col lg:flex-row justify-between gap-3 w-full mb-3 h-4/5">
<section class="w-full lg:w-2/5 h-full"> <section class="w-full lg:w-2/5 h-full">
<strong class="my-2 text-base font-semibold text-black dark:text-gray-100" <strong class="my-2 text-base font-semibold text-black dark:text-gray-100"
>Certificate settings</strong >{$t('course.navItem.certificates.certificate_settings')}</strong
> >
<p class="text-xs font-normal my-4 dark:text-gray-100">Choose a theme</p> <p class="text-xs font-normal my-4 dark:text-gray-100">
{$t('course.navItem.certificates.theme')}
</p>
<RadioButtonGroup <RadioButtonGroup
bind:selected={$course.certificate_theme} bind:selected={$course.certificate_theme}
class="mb-10" class="mb-10"
@@ -72,42 +75,48 @@
{/each} {/each}
</RadioButtonGroup> </RadioButtonGroup>
<div> <div>
<p class="text-xs font-normal text-black my-2 dark:text-gray-100">Brand Logo</p> <p class="text-xs font-normal text-black my-2 dark:text-gray-100">
{$t('course.navItem.certificates.logo')}
</p>
<div> <div>
<p class="text-base mt-1 dark:text-gray-100"> <p class="text-base mt-1 dark:text-gray-100">
To update your brand image, go to <strong class="font-semibold" {$t('course.navItem.certificates.to_update')}
>Settings &gt; Organisation settings</strong <strong class="font-semibold">{$t('course.navItem.certificates.settings')}</strong>
> and upload your brand logo {$t('course.navItem.certificates.and_upload')}
</p> </p>
<PrimaryButton <PrimaryButton
label="Go to Settings" label={$t('course.navItem.certificates.goto_settings')}
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
className="rounded-md mt-3" className="rounded-md mt-3"
onClick={() => goto(`/org/${$currentOrg.siteName}/settings`)} onClick={() => goto(`/org/${$currentOrg.siteName}/settings`)}
/> />
</div> </div>
<span class="my-4"> <span class="my-4">
<p class="dark:text-gray-100 text-xs font-normal mt-4 mb-2">Course Description</p> <p class="dark:text-gray-100 text-xs font-normal mt-4 mb-2">
{$t('course.navItem.certificates.description')}
</p>
<TextArea <TextArea
rows={6} rows={6}
placeholder="a little description about the course" placeholder={$t('course.navItem.certificates.placeholder')}
bind:value={$course.description} bind:value={$course.description}
errorMessage={errors.description} errorMessage={errors.description}
disabled={$isFreePlan} disabled={$isFreePlan}
/> />
</span> </span>
<Toggle <Toggle
labelText="Allow students download certificate" labelText={$t('course.navItem.certificates.allow')}
bind:toggled={$course.is_certificate_downloadable} bind:toggled={$course.is_certificate_downloadable}
class="my-4" class="my-4"
size="sm" size="sm"
disabled={$isFreePlan} disabled={$isFreePlan}
> >
<span slot="labelA" style={$globalStore.isDark ? 'color: white' : 'color: #161616'} <span slot="labelA" style={$globalStore.isDark ? 'color: white' : 'color: #161616'}
>Locked</span >{$t('course.navItem.certificates.locked')}</span
>
<span slot="labelB" style="color: green"
>{$t('course.navItem.certificates.unlocked')}</span
> >
<span slot="labelB" style="color: green">Unlocked</span>
</Toggle> </Toggle>
</div> </div>
</section> </section>
@@ -134,7 +143,7 @@
{#if $isFreePlan} {#if $isFreePlan}
<FlashFilled size={16} class="text-blue-700" /> <FlashFilled size={16} class="text-blue-700" />
{/if} {/if}
Save Changes {$t('course.navItem.certificates.save')}
</PrimaryButton> </PrimaryButton>
</div> </div>
</main> </main>
@@ -1,14 +1,17 @@
<script> <script>
import Box from '$lib/components/Box/index.svelte'; import Box from '$lib/components/Box/index.svelte';
import { t } from '$lib/utils/functions/translations';
</script> </script>
<Box> <Box>
<div class="flex flex-col items-center justify-center w-max h-full gap-2"> <div class="flex flex-col items-center justify-center w-max h-full gap-2">
<img src="/professional.png" alt="Certificate" /> <img src="/professional.png" alt="Certificate" />
<p class="text-xl font-normal text-center">Certificates not available</p> <p class="text-xl font-normal text-center">
{$t('course.navItem.certificates.locked_certificate')}
</p>
<p class="text-sm font-normal text-center"> <p class="text-sm font-normal text-center">
Your instructor needs to unlock your certificate. <br /> If you need your certificate, please contact {$t('course.navItem.certificates.locked_certificate_body_1')} <br />
your instructor. {$t('course.navItem.certificates.locked_certificate_body_2')}
</p> </p>
</div> </div>
</Box> </Box>
@@ -5,6 +5,7 @@
import { profile } from '$lib/utils/store/user'; import { profile } from '$lib/utils/store/user';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import Box from '$lib/components/Box/index.svelte'; import Box from '$lib/components/Box/index.svelte';
import { t } from '$lib/utils/functions/translations';
let isLoading = false; let isLoading = false;
@@ -44,9 +45,11 @@
<Box> <Box>
<div class="flex flex-col items-center justify-center w-max h-full gap-5"> <div class="flex flex-col items-center justify-center w-max h-full gap-5">
<img src="/professional.png" alt="Certificate" /> <img src="/professional.png" alt="Certificate" />
<p class="text-xl font-normal text-center">Certificate is now available</p> <p class="text-xl font-normal text-center">
{$t('course.navItem.certificates.unlocked_certificate')}
</p>
<PrimaryButton <PrimaryButton
label="DownLoad Certificate" label={$t('course.navItem.certificates.download_certificate')}
className="rounded-md" className="rounded-md"
onClick={downLoadCertificate} onClick={downLoadCertificate}
isDisabled={!PUBLIC_SERVER_URL} isDisabled={!PUBLIC_SERVER_URL}
@@ -14,6 +14,7 @@
import Modal from '$lib/components/Modal/index.svelte'; import Modal from '$lib/components/Modal/index.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { handleAddLessonWidget } from '../Navigation/store'; import { handleAddLessonWidget } from '../Navigation/store';
import { t } from '$lib/utils/functions/translations';
export let isStudent = false; export let isStudent = false;
let errors = { let errors = {
@@ -56,9 +57,9 @@
<Modal <Modal
onClose={() => ($handleAddLessonWidget.open = false)} onClose={() => ($handleAddLessonWidget.open = false)}
bind:open={$handleAddLessonWidget.open} bind:open={$handleAddLessonWidget.open}
width="w-[80%] md:w-[60%]" width="w-[80%] md:w-[65%]"
containerClass="overflow-hidden" containerClass="overflow-hidden"
modalHeading="Add New Lesson" modalHeading={$t('course.navItem.lessons.add_lesson.modal_heading')}
> >
<div <div
class="sm:min-h-[245px] md:min-h-[100px] lg:min-h-[190px] relative m-auto rounded-md border-2 border-gray-200 dark:border-neutral-600 py-2 md:py-3 px-2 md:px-5 mb-2 md:mb-4 flex flex-wrap items-center dark:bg-neutral-800" class="sm:min-h-[245px] md:min-h-[100px] lg:min-h-[190px] relative m-auto rounded-md border-2 border-gray-200 dark:border-neutral-600 py-2 md:py-3 px-2 md:px-5 mb-2 md:mb-4 flex flex-wrap items-center dark:bg-neutral-800"
@@ -75,7 +76,7 @@
<div class="w-full md:w-4/5"> <div class="w-full md:w-4/5">
<div class="flex flex-row gap-1 md:gap-2"> <div class="flex flex-row gap-1 md:gap-2">
<TextField <TextField
label="Lesson title" label={$t('course.navItem.lessons.add_lesson.lesson_title')}
bind:value={lesson.title} bind:value={lesson.title}
autofocus={true} autofocus={true}
className="flex-1 min-w-lg max-w-lg" className="flex-1 min-w-lg max-w-lg"
@@ -87,7 +88,11 @@
toolTipProps={isStudent toolTipProps={isStudent
? {} ? {}
: { : {
title: `Click to ${lesson.is_unlocked ? 'lock' : 'unlock'}`, title: `${$t('course.navItem.lessons.add_lesson.click_to')} ${
lesson.is_unlocked
? $t('course.navItem.lessons.add_lesson.lock')
: $t('course.navItem.lessons.add_lesson.unlock')
}`,
direction: 'right' direction: 'right'
}} }}
> >
@@ -131,5 +136,5 @@
</div> </div>
</div> </div>
</div> </div>
<PrimaryButton label="Save" onClick={handleSave} /> <PrimaryButton label={$t('course.navItem.lessons.add_lesson.save')} onClick={handleSave} />
</Modal> </Modal>
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
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 { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { t } from '$lib/utils/functions/translations';
export let lessonTitle: string; export let lessonTitle: string;
export let openDeleteModal = false; export let openDeleteModal = false;
@@ -18,24 +18,24 @@
onClose={() => (openDeleteModal = false)} onClose={() => (openDeleteModal = false)}
bind:open={openDeleteModal} bind:open={openDeleteModal}
width="w-96" width="w-96"
modalHeading="Delete" modalHeading={$t('course.navItem.lessons.delete_lesson.modal_heading')}
> >
<div> <div>
<h1 class="dark:text-white text-lg"> <h1 class="dark:text-white text-lg">
Are you sure you want to delete: <strong>{lessonTitle}</strong>? {$t('course.navItem.lessons.delete_lesson.content')} <strong>{lessonTitle}</strong>?
</h1> </h1>
<div class="mt-5 flex items-center justify-between"> <div class="mt-5 flex items-center justify-between">
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="No" label={$t('course.navItem.lessons.delete_lesson.no')}
onClick={() => (openDeleteModal = false)} onClick={() => (openDeleteModal = false)}
/> />
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="Yes" label={$t('course.navItem.lessons.delete_lesson.yes')}
onClick={handleDelete} onClick={handleDelete}
/> />
</div> </div>
@@ -1,6 +1,7 @@
<script> <script>
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 { t } from '$lib/utils/functions/translations';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { deleteConfirmation } from '../store/exercise'; import { deleteConfirmation } from '../store/exercise';
@@ -18,16 +19,18 @@
onClose={() => ($deleteConfirmation.open = false)} onClose={() => ($deleteConfirmation.open = false)}
bind:open={$deleteConfirmation.open} bind:open={$deleteConfirmation.open}
width="w-96" width="w-96"
modalHeading="Delete question" modalHeading={$t('course.navItem.lessons.exercises.all_exercises.delete_confirmation')}
> >
<div> <div>
<h1 class="dark:text-white text-lg">Are you sure you want to delete this question?</h1> <h1 class="dark:text-white text-lg">
{$t('course.navItem.lessons.exercises.all_exercises.sure')}
</h1>
<div class="mt-5 flex items-center justify-between"> <div class="mt-5 flex items-center justify-between">
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="No, please don't" label={$t('course.navItem.lessons.exercises.all_exercises.no')}
onClick={() => { onClick={() => {
$deleteConfirmation.open = false; $deleteConfirmation.open = false;
onCancel(); onCancel();
@@ -36,7 +39,7 @@
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
variant={VARIANTS.CONTAINED} variant={VARIANTS.CONTAINED}
label="Yes, delete" label={$t('course.navItem.lessons.exercises.all_exercises.yes')}
onClick={() => { onClick={() => {
onDelete(); onDelete();
$deleteConfirmation.open = false; $deleteConfirmation.open = false;
@@ -33,6 +33,7 @@
import { filterOutDeleted } from './functions'; import { filterOutDeleted } from './functions';
import { deleteExercise } from '$lib/utils/services/courses'; import { deleteExercise } from '$lib/utils/services/courses';
import { lesson } from '../store/lessons'; import { lesson } from '../store/lessons';
import { t } from '$lib/utils/functions/translations';
const initialQuestionsLength = $questionnaire.questions.length; const initialQuestionsLength = $questionnaire.questions.length;
@@ -95,23 +96,27 @@
onClose={() => (shouldDeleteExercise = false)} onClose={() => (shouldDeleteExercise = false)}
bind:open={shouldDeleteExercise} bind:open={shouldDeleteExercise}
width="w-2/4" width="w-2/4"
modalHeading="Delete Modal" modalHeading={$t('course.navItem.lessons.exercises.all_exercises.edit_mode.delete_modal')}
> >
<form on:submit|preventDefault> <form on:submit|preventDefault>
<h1 class="dark:text-white text-xl">Are you sure you want to delete this exercise?</h1> <h1 class="dark:text-white text-xl">
{$t('course.navItem.lessons.exercises.all_exercises.edit_mode.sure')}
</h1>
<div class="mt-5 flex items-center justify-between"> <div class="mt-5 flex items-center justify-between">
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="No, cancel" label={$t('course.navItem.lessons.exercises.all_exercises.edit_mode.no')}
type="submit" type="submit"
onClick={() => (shouldDeleteExercise = false)} onClick={() => (shouldDeleteExercise = false)}
/> />
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
variant={VARIANTS.CONTAINED} variant={VARIANTS.CONTAINED}
label={isDeleting ? 'Deleting...' : 'Yes, delete'} label={isDeleting
? $t('course.navItem.lessons.exercises.all_exercises.edit_mode.deleting')
: $t('course.navItem.lessons.exercises.all_exercises.edit_mode.yes')}
isDisabled={isDeleting} isDisabled={isDeleting}
onClick={handleDelete} onClick={handleDelete}
/> />
@@ -122,7 +127,9 @@
<div class="w-full"> <div class="w-full">
{#if Object.values(errors).length} {#if Object.values(errors).length}
<div class="w-full flex justify-center mb-4"> <div class="w-full flex justify-center mb-4">
<ErrorMessage message="You have some errors" /> <ErrorMessage
message={$t('course.navItem.lessons.exercises.all_exercises.edit_mode.error')}
/>
</div> </div>
{/if} {/if}
<div class="questions pt-6 px-6"> <div class="questions pt-6 px-6">
@@ -140,7 +147,7 @@
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="mr-5 w-3/5"> <div class="mr-5 w-3/5">
<TextField <TextField
placeholder="Question" placeholder={$t('course.navItem.lessons.exercises.all_exercises.edit_mode.question')}
bind:value={question.title} bind:value={question.title}
isRequired={true} isRequired={true}
onChange={() => { onChange={() => {
@@ -160,14 +167,18 @@
}} }}
> >
{#each QUESTION_TYPES as type} {#each QUESTION_TYPES as type}
<SelectItem value={type.id} text={type.label} /> <SelectItem value={type.id} text={$t(type.label)} />
{/each} {/each}
</Select> </Select>
</div> </div>
{#if typeof question.code === 'string'} {#if typeof question.code === 'string'}
<div class="flex justify-between items-center my-3 w-3/5"> <div class="flex justify-between items-center my-3 w-3/5">
<TextArea bind:value={question.code} rows="2" placeholder="Write your code" /> <TextArea
bind:value={question.code}
rows="2"
placeholder={$t('course.navItem.lessons.exercises.all_exercises.edit_mode.write')}
/>
<IconButton value="write-code" onClick={() => handleCode(question.id, false)}> <IconButton value="write-code" onClick={() => handleCode(question.id, false)}>
<TrashCanIcon size={24} class="carbon-icon dark:text-white" /> <TrashCanIcon size={24} class="carbon-icon dark:text-white" />
</IconButton> </IconButton>
@@ -255,7 +266,9 @@
onClick={handleAddOption(question.id)} onClick={handleAddOption(question.id)}
> >
<AddFilledIcon size={24} class="carbon-icon dark:text-white" /> <AddFilledIcon size={24} class="carbon-icon dark:text-white" />
<p class="dark:text-white ml-2">Add option</p> <p class="dark:text-white ml-2">
{$t('course.navItem.lessons.exercises.all_exercises.edit_mode.option')}
</p>
</PrimaryButton> </PrimaryButton>
</div> </div>
{/if} {/if}
@@ -38,7 +38,7 @@
total total
}); });
snackbar.success(`Submission updated to '${status.label}'`); snackbar.success(`snackbar.exercise.submission_updated '${status.label}'`);
} }
function setStatus(data) { function setStatus(data) {
@@ -9,6 +9,7 @@
import { questionnaireOrder, questionnaire } from '../store/exercise'; import { questionnaireOrder, questionnaire } from '../store/exercise';
import { filterOutDeleted } from './functions'; import { filterOutDeleted } from './functions';
import { t } from '$lib/utils/functions/translations';
const flipDurationMs = 300; const flipDurationMs = 300;
@@ -54,7 +55,7 @@
onClose={handleClose} onClose={handleClose}
bind:open={$questionnaireOrder.open} bind:open={$questionnaireOrder.open}
width="w-96" width="w-96"
modalHeading="Order questions" modalHeading={$t('course.navItem.lessons.exercises.all_exercises.order_questions')}
> >
<section <section
use:dndzone={{ use:dndzone={{
@@ -3,6 +3,7 @@
import type { ExerciseSubmissions } from '$lib/utils/types'; import type { ExerciseSubmissions } from '$lib/utils/types';
import { questionnaire } from '../../store/exercise'; import { questionnaire } from '../../store/exercise';
import { submissions } from './store'; import { submissions } from './store';
import { t } from '$lib/utils/functions/translations';
export let isLoading = false; export let isLoading = false;
@@ -45,7 +46,7 @@
(ans: { question_id: number }) => ans.question_id === q.id (ans: { question_id: number }) => ans.question_id === q.id
); );
if (filteredAnswer.some((ans: { open_answer: string }) => ans.open_answer == '')) { if (filteredAnswer.some((ans: { open_answer: string }) => ans.open_answer == '')) {
return 'No Answer was provided'; return $t('course.navItem.lessons.exercise.all_exercises.analytics.individual.no');
} else { } else {
return filteredAnswer.map((ans: { open_answer: any }) => ans.open_answer); return filteredAnswer.map((ans: { open_answer: any }) => ans.open_answer);
} }
@@ -79,7 +80,9 @@
</div> </div>
<p class="font-medium mb-2"> <p class="font-medium mb-2">
{$submissions[studentSelected].submitted_by.profile.fullname}'s Individual Answers {$submissions[studentSelected].submitted_by.profile.fullname}'s {$t(
'course.navItem.lessons.exercise.all_exercises.analytics.individual.answers'
)}
</p> </p>
{#if $submissions[studentSelected]} {#if $submissions[studentSelected]}
{#if $questionnaire.questions} {#if $questionnaire.questions}
@@ -14,6 +14,7 @@
import { submissions } from './store'; import { submissions } from './store';
import '@carbon/charts-svelte/styles.css'; import '@carbon/charts-svelte/styles.css';
import { Loading } from 'carbon-components-svelte'; import { Loading } from 'carbon-components-svelte';
import { t } from '$lib/utils/functions/translations';
export let isLoading = true; export let isLoading = true;
@@ -142,7 +143,9 @@
<Loading withOverlay={true} /> <Loading withOverlay={true} />
{:else if browser} {:else if browser}
<div> <div>
<p class="text-2xl mb-3">Question Charts</p> <p class="text-2xl mb-3">
{$t('course.navItem.lessons.exercise.all_exercises.analytics.summary.question_chart')}
</p>
{#each transformedQuestions as q} {#each transformedQuestions as q}
<div class="mb-4"> <div class="mb-4">
<p>{q.title}</p> <p>{q.title}</p>
@@ -4,12 +4,19 @@
import Summary from './Summary.svelte'; import Summary from './Summary.svelte';
import Individual from './Individual.svelte'; import Individual from './Individual.svelte';
import { submissions } from './store'; import { submissions } from './store';
import { t } from '$lib/utils/functions/translations';
export let exerciseId: string; export let exerciseId: string;
const tabs = [ const tabs = [
{ label: 'Summary', value: 'summary' }, {
{ label: 'Individual', value: 'individual' } label: $t('course.navItem.lessons.exercise.all_exercises.analytics.summary.heading'),
value: 'summary'
},
{
label: $t('course.navItem.lessons.exercise.all_exercises.analytics.individual.heading'),
value: 'individual'
}
]; ];
let currentTab = tabs[0].value; let currentTab = tabs[0].value;
@@ -38,7 +45,10 @@
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<div class="border rounded-md mb-2 px-4"> <div class="border rounded-md mb-2 px-4">
<div class="mt-4"> <div class="mt-4">
<p class="text-lg">{$submissions.length} Submissions</p> <p class="text-lg">
{$submissions.length}
{$t('course.navItem.lessons.exercise.all_exercises.analytics.submissions')}
</p>
</div> </div>
<div class="flex items-center justify-center overflow-x-auto"> <div class="flex items-center justify-center overflow-x-auto">
{#each tabs as tab} {#each tabs as tab}
@@ -4,6 +4,7 @@
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import { questionnaire } from '../store/exercise'; import { questionnaire } from '../store/exercise';
import TextEditor from '$lib/components/TextEditor/index.svelte'; import TextEditor from '$lib/components/TextEditor/index.svelte';
import { t } from '$lib/utils/functions/translations';
export let preview: boolean; export let preview: boolean;
@@ -19,13 +20,13 @@
<QuestionContainer isTitle={true}> <QuestionContainer isTitle={true}>
{#if !preview} {#if !preview}
<TextField <TextField
placeholder="Title" placeholder={$t('course.navItem.lessons.exercises.all_exercises.description.title')}
bind:value={$questionnaire.title} bind:value={$questionnaire.title}
className="mb-2" className="mb-2"
onChange={() => ($questionnaire.is_title_dirty = true)} onChange={() => ($questionnaire.is_title_dirty = true)}
/> />
<DateTime <DateTime
label="Due by" label={$t('course.navItem.lessons.exercises.all_exercises.view_mode.due')}
className="w-50" className="w-50"
value={$questionnaire.due_by} value={$questionnaire.due_by}
onInput={(e) => { onInput={(e) => {
@@ -35,14 +36,16 @@
/> />
<div class="mt-3"> <div class="mt-3">
<p class="mb-1">Description</p> <p class="mb-1">
{$t('course.navItem.lessons.exercises.all_exercises.description.heading')}
</p>
<TextEditor <TextEditor
value={$questionnaire.description} value={$questionnaire.description}
onChange={(html) => { onChange={(html) => {
$questionnaire.is_description_dirty = true; $questionnaire.is_description_dirty = true;
$questionnaire.description = html; $questionnaire.description = html;
}} }}
placeholder="Describe the exercise to your student" placeholder={$t('course.navItem.lessons.exercises.all_exercises.description.describe')}
maxHeight={300} maxHeight={300}
/> />
</div> </div>
@@ -50,25 +53,30 @@
<h2 class="my-1">{$questionnaire.title}</h2> <h2 class="my-1">{$questionnaire.title}</h2>
<div class="flex items-center"> <div class="flex items-center">
<p class="dark:text-white mx-2"> <p class="dark:text-white mx-2">
<strong>{$questionnaire.questions.length}</strong> questions <strong>{$questionnaire.questions.length}</strong>
{$t('course.navItem.lessons.exercises.all_exercises.view_mode.questions')}
</p> </p>
| |
<p class="dark:text-white mx-2"> <p class="dark:text-white mx-2">
<strong>{getTotalPossibleGrade($questionnaire.questions)}</strong> points. <strong>{getTotalPossibleGrade($questionnaire.questions)}</strong>
{$t('course.navItem.lessons.exercises.all_exercises.view_mode.points')}.
</p> </p>
| |
<p class="dark:text-white mx-2">All required</p> <p class="dark:text-white mx-2">
{$t('course.navItem.lessons.exercises.all_exercises.view_mode.all')}
</p>
{#if $questionnaire.due_by} {#if $questionnaire.due_by}
| |
<p class="dark:text-white mx-2"> <p class="dark:text-white mx-2">
<strong>Due by:</strong> <strong>{$t('course.navItem.lessons.exercises.all_exercises.view_mode.due')}:</strong>
{new Date($questionnaire.due_by).toLocaleString()} {new Date($questionnaire.due_by).toLocaleString()}
</p> </p>
{/if} {/if}
</div> </div>
<article class="mt-3 preview prose prose-sm sm:prose p-2"> <article class="mt-3 preview prose prose-sm sm:prose p-2">
{@html $questionnaire.description || 'No description'} {@html $questionnaire.description ||
$t('course.navItem.lessons.exercises.all_exercises.description.no')}
</article> </article>
{/if} {/if}
</QuestionContainer> </QuestionContainer>
@@ -30,6 +30,7 @@
import UpdateDescription from './UpdateDescription.svelte'; import UpdateDescription from './UpdateDescription.svelte';
import { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
import Analytics from './Submissions/index.svelte'; import Analytics from './Submissions/index.svelte';
import { t } from '$lib/utils/functions/translations';
export let exerciseId = ''; export let exerciseId = '';
export let path = ''; export let path = '';
@@ -61,7 +62,7 @@
// @ts-ignore // @ts-ignore
questions: updatedQuestions questions: updatedQuestions
})); }));
snackbar.success('Saved Successfully'); snackbar.success('snackbar.exercise.success');
// Redirect to exercises page // Redirect to exercises page
goto(path); goto(path);
@@ -83,7 +84,9 @@
<PageBody bind:isPageNavHidden={isStudent} padding="px-4 overflow-x-hidden"> <PageBody bind:isPageNavHidden={isStudent} padding="px-4 overflow-x-hidden">
<div class="bg-gray-100 dark:bg-neutral-800 top-0 z-10 sticky p-2 mb-3"> <div class="bg-gray-100 dark:bg-neutral-800 top-0 z-10 sticky p-2 mb-3">
<Breadcrumb noTrailingSlash class="mb-5"> <Breadcrumb noTrailingSlash class="mb-5">
<BreadcrumbItem href={path}>All Exercises</BreadcrumbItem> <BreadcrumbItem href={path}
>{$t('course.navItem.lessons.exercises.all_exercises.heading')}</BreadcrumbItem
>
<BreadcrumbItem href={`${path}${exerciseId}`} isCurrentPage> <BreadcrumbItem href={`${path}${exerciseId}`} isCurrentPage>
{$questionnaire.title} {$questionnaire.title}
</BreadcrumbItem> </BreadcrumbItem>
@@ -91,8 +94,15 @@
<RoleBasedSecurity allowedRoles={[1, 2]}> <RoleBasedSecurity allowedRoles={[1, 2]}>
<ContentSwitcher bind:selectedIndex class="mb-2"> <ContentSwitcher bind:selectedIndex class="mb-2">
<Switch text="Questions ({$questionnaire.questions.length})" /> <Switch
<Switch text="Submissions ({$questionnaire.totalSubmissions})" /> text="{$t('course.navItem.lessons.exercises.all_exercises.questions')} ({$questionnaire
.questions.length})"
/>
<Switch
text="{$t(
'course.navItem.lessons.exercises.all_exercises.submissions'
)} ({$questionnaire.totalSubmissions})"
/>
</ContentSwitcher> </ContentSwitcher>
{#if selectedIndex === 0} {#if selectedIndex === 0}
@@ -101,7 +111,7 @@
<PrimaryButton <PrimaryButton
className="mr-2" className="mr-2"
variant={VARIANTS.CONTAINED} variant={VARIANTS.CONTAINED}
label="Save" label={$t('course.navItem.lessons.exercises.all_exercises.save')}
onClick={handleSave} onClick={handleSave}
isLoading={isSaving} isLoading={isSaving}
/> />
@@ -109,7 +119,7 @@
onClick={() => (preview = !preview)} onClick={() => (preview = !preview)}
contained={preview} contained={preview}
toolTipProps={{ toolTipProps={{
title: 'Preview', title: $t('course.navItem.lessons.exercises.all_exercises.preview'),
direction: 'bottom', direction: 'bottom',
hotkeys: [] hotkeys: []
}} }}
@@ -125,12 +135,12 @@
</IconButton> </IconButton>
<OverflowMenu flipped> <OverflowMenu flipped>
<OverflowMenuItem <OverflowMenuItem
text="Reorder Questions" text={$t('course.navItem.lessons.exercises.all_exercises.reorder')}
on:click={() => ($questionnaireOrder.open = true)} on:click={() => ($questionnaireOrder.open = true)}
/> />
<OverflowMenuItem <OverflowMenuItem
danger danger
text="Delete Exercise" text={$t('course.navItem.lessons.exercises.all_exercises.delete_exercise')}
on:click={() => (shouldDeleteExercise = true)} on:click={() => (shouldDeleteExercise = true)}
/> />
</OverflowMenu> </OverflowMenu>
@@ -17,6 +17,7 @@
import { getTextFromHTML } from '$lib/utils/functions/toHtml'; import { getTextFromHTML } from '$lib/utils/functions/toHtml';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { Circle3 } from 'svelte-loading-spinners'; import { Circle3 } from 'svelte-loading-spinners';
import { t } from '$lib/utils/functions/translations';
export let open = false; export let open = false;
export let handleAddExercise = () => {}; export let handleAddExercise = () => {};
@@ -40,20 +41,20 @@
const options = [ const options = [
{ {
title: 'From Scratch', title: $t('course.navItem.lessons.exercises.new_exercise_modal.options.1'),
subtitle: 'Create your exercise from scratch if you have your questions already prepared', subtitle: $t('course.navItem.lessons.exercises.new_exercise_modal.options.subtitle_1'),
type: Type.SCRATCH, type: Type.SCRATCH,
isDisabled: false isDisabled: false
}, },
{ {
title: 'Use a Template', title: $t('course.navItem.lessons.exercises.new_exercise_modal.options.2'),
subtitle: 'Select from 100+ templates of predefined exercise to help you get started', subtitle: $t('course.navItem.lessons.exercises.new_exercise_modal.options.subtitle_2'),
type: Type.TEMPLATE, type: Type.TEMPLATE,
isDisabled: false isDisabled: false
}, },
{ {
title: 'Use AI', title: $t('course.navItem.lessons.exercises.new_exercise_modal.options.3'),
subtitle: 'You can generate an exercise with AI from your notes', subtitle: $t('course.navItem.lessons.exercises.new_exercise_modal.options.subtitle_3'),
type: Type.AI, type: Type.AI,
isDisabled: false isDisabled: false
} }
@@ -105,7 +106,7 @@
<Modal <Modal
onClose={handleCancelAddExercise} onClose={handleCancelAddExercise}
bind:open bind:open
modalHeading="New Exercise" modalHeading={$t('course.navItem.lessons.exercises.new_exercise_modal.heading')}
maxWidth="max-w-4xl" maxWidth="max-w-4xl"
width="w-4/5" width="w-4/5"
> >
@@ -114,7 +115,9 @@
{/if} {/if}
{#if step === 0} {#if step === 0}
<div> <div>
<h2 class="text-2xl font-medium my-5">How do you want to create your exercise?</h2> <h2 class="text-2xl font-medium my-5">
{$t('course.navItem.lessons.exercises.new_exercise_modal.how')}?
</h2>
<div class="flex gap-2 justify-between my-8"> <div class="flex gap-2 justify-between my-8">
{#each options as option} {#each options as option}
@@ -154,35 +157,49 @@
</div> </div>
<div class="mt-8 flex items-center flex-row-reverse"> <div class="mt-8 flex items-center flex-row-reverse">
<PrimaryButton className="px-6 py-3" label="Next" onClick={handleNext} /> <PrimaryButton
className="px-6 py-3"
label={$t('course.navItem.lessons.exercises.new_exercise_modal.next')}
onClick={handleNext}
/>
</div> </div>
</div> </div>
{:else if step === 1} {:else if step === 1}
{#if type === Type.SCRATCH} {#if type === Type.SCRATCH}
<div class="flex items-center justify-center w-96 m-auto min-h-[300px]"> <div class="flex items-center justify-center w-96 m-auto min-h-[300px]">
<div class="w-full"> <div class="w-full">
<h2 class="text-2xl font-medium my-5">Give your exercise a title</h2> <h2 class="text-2xl font-medium my-5">
{$t('course.navItem.lessons.exercises.new_exercise_modal.title')}
</h2>
<TextField <TextField
bind:value={title} bind:value={title}
autoFocus={true} autoFocus={true}
placeholder="Exercise name" placeholder={$t(
'course.navItem.lessons.exercises.new_exercise_modal.title_placeholder'
)}
className="my-4" className="my-4"
/> />
<div class="mt-5 flex items-center justify-between"> <div class="mt-5 flex items-center justify-between">
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
label="Back" label={$t('course.navItem.lessons.exercises.new_exercise_modal.back')}
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
onClick={handleBack} onClick={handleBack}
/> />
<PrimaryButton className="px-6 py-3" label="Finish" onClick={handleAddExercise} /> <PrimaryButton
className="px-6 py-3"
label={$t('course.navItem.lessons.exercises.new_exercise_modal.finish')}
onClick={handleAddExercise}
/>
</div> </div>
</div> </div>
</div> </div>
{:else if type === Type.TEMPLATE} {:else if type === Type.TEMPLATE}
<div> <div>
<h2 class="text-2xl font-medium my-5">Select a template</h2> <h2 class="text-2xl font-medium my-5">
{$t('course.navItem.lessons.exercises.new_exercise_modal.select_template')}
</h2>
<div> <div>
<div class="mb-5 flex items-center gap-2"> <div class="mb-5 flex items-center gap-2">
@@ -215,10 +232,12 @@
</p> </p>
<div class="flex flex-col items-start justify-between gap-1"> <div class="flex flex-col items-start justify-between gap-1">
<p class="text-xs font-normal"> <p class="text-xs font-normal">
{template.questions} Questions {template.questions}
{$t('course.navItem.lessons.exercises.new_exercise_modal.questions')}
</p> </p>
<p class="text-xs font-normal text-start"> <p class="text-xs font-normal text-start">
{template.points} Points {template.points}
{$t('course.navItem.lessons.exercises.new_exercise_modal.points')}
</p> </p>
</div> </div>
</div> </div>
@@ -229,14 +248,14 @@
<div class="mt-5 flex items-center justify-between"> <div class="mt-5 flex items-center justify-between">
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
label="Back" label={$t('course.navItem.lessons.exercises.new_exercise_modal.back')}
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
onClick={handleBack} onClick={handleBack}
/> />
<PrimaryButton <PrimaryButton
isDisabled={!selectedTemplateId} isDisabled={!selectedTemplateId}
className="px-6 py-3" className="px-6 py-3"
label="Finish" label={$t('course.navItem.lessons.exercises.new_exercise_modal.finish')}
isLoading={isTemplateFinishedLoading} isLoading={isTemplateFinishedLoading}
onClick={async () => { onClick={async () => {
isTemplateFinishedLoading = true; isTemplateFinishedLoading = true;
@@ -256,13 +275,12 @@
<div class="flex flex-row justify-between max-h-[500px]"> <div class="flex flex-row justify-between max-h-[500px]">
<div class="w-[60%] mr-1 border px-3 py-2 rounded-md"> <div class="w-[60%] mr-1 border px-3 py-2 rounded-md">
{#if note.length} {#if note.length}
<h3>Create exercises from Notes with AI</h3> <h3>{$t('course.navItem.lessons.exercises.new_exercise_modal.create_exercises')}</h3>
<p class="text-sm mb-4"> <p class="text-sm mb-4">
Choose how many questions and options you want and AI will help you create an {$t('course.navItem.lessons.exercises.new_exercise_modal.choose_questions')}
exercise out of your note. Let's go.
</p> </p>
<TextField <TextField
label="How many questions do you want to create?" label={$t('course.navItem.lessons.exercises.new_exercise_modal.how_many_questions')}
type="number" type="number"
bind:value={questionNumber} bind:value={questionNumber}
placeholder="5" placeholder="5"
@@ -270,14 +288,14 @@
isRequired isRequired
/> />
<TextField <TextField
label="How many options per questions do you want?" label={$t('course.navItem.lessons.exercises.new_exercise_modal.how_many_options')}
type="number" type="number"
bind:value={optionNumber} bind:value={optionNumber}
placeholder="5" placeholder="5"
isRequired isRequired
/> />
{:else} {:else}
<h3>Please add a note to use this feature</h3> <h3>{$t('course.navItem.lessons.exercises.new_exercise_modal.add_note')}</h3>
{/if} {/if}
<div class="mt-5 flex items-center flex-row-reverse"> <div class="mt-5 flex items-center flex-row-reverse">
<PrimaryButton <PrimaryButton
@@ -286,7 +304,7 @@
isDisabled={$isLoading || !note} isDisabled={$isLoading || !note}
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
> >
Generate {$t('course.navItem.lessons.exercises.new_exercise_modal.generate')}
</PrimaryButton> </PrimaryButton>
</div> </div>
</div> </div>
@@ -296,23 +314,27 @@
{#if $isLoading} {#if $isLoading}
<Circle3 size="60" unit="px" duration="1s" /> <Circle3 size="60" unit="px" duration="1s" />
{:else if isAIStarted} {:else if isAIStarted}
<p class="max-h-[200px] leading-7 text-sm">AI Generation Complete</p> <p class="max-h-[200px] leading-7 text-sm">
{$t('course.navItem.lessons.exercises.new_exercise_modal.completion')}
</p>
{:else} {:else}
<p class="max-h-[200px] leading-7 text-sm">Click "Generate" for some magic</p> <p class="max-h-[200px] leading-7 text-sm">
{$t('course.navItem.lessons.exercises.new_exercise_modal.click_generate')}
</p>
{/if} {/if}
</div> </div>
</div> </div>
<div class="mt-5 flex items-center justify-between"> <div class="mt-5 flex items-center justify-between">
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
label="Back" label={$t('course.navItem.lessons.exercises.new_exercise_modal.back')}
variant={VARIANTS.TEXT} variant={VARIANTS.TEXT}
onClick={handleBack} onClick={handleBack}
/> />
<PrimaryButton <PrimaryButton
isDisabled={$isLoading || !note} isDisabled={$isLoading || !note}
className="px-6 py-3" className="px-6 py-3"
label="Finish" label={$t('course.navItem.lessons.exercises.new_exercise_modal.finish')}
onClick={handleAddExercise} onClick={handleAddExercise}
/> />
</div> </div>
@@ -17,6 +17,7 @@
import NewExerciseModal from '$lib/components/Course/components/Lesson/Exercises/NewExerciseModal.svelte'; import NewExerciseModal from '$lib/components/Course/components/Lesson/Exercises/NewExerciseModal.svelte';
import type { ExerciseTemplate } from '$lib/utils/types'; import type { ExerciseTemplate } from '$lib/utils/types';
import { formatDate } from '$lib/utils/functions/routes/dashboard'; import { formatDate } from '$lib/utils/functions/routes/dashboard';
import { t } from '$lib/utils/functions/translations';
export let path = ''; export let path = '';
export let exerciseId = ''; export let exerciseId = '';
@@ -163,10 +164,15 @@
<PageBody bind:isPageNavHidden={isStudent}> <PageBody bind:isPageNavHidden={isStudent}>
<slot:fragment slot="header"> <slot:fragment slot="header">
<Breadcrumb class="my-2"> <Breadcrumb class="my-2">
<BreadcrumbItem href={path}>All Exercises</BreadcrumbItem> <BreadcrumbItem href={path}>{$t('course.navItem.lessons.exercises.heading')}</BreadcrumbItem
>
</Breadcrumb> </Breadcrumb>
<RoleBasedSecurity allowedRoles={[1, 2]}> <RoleBasedSecurity allowedRoles={[1, 2]}>
<PrimaryButton className="mr-2 my-2" label="Add" onClick={() => (open = !open)} /> <PrimaryButton
className="mr-2 my-2"
label={$t('course.navItem.lessons.exercises.add_button')}
onClick={() => (open = !open)}
/>
</RoleBasedSecurity> </RoleBasedSecurity>
</slot:fragment> </slot:fragment>
@@ -183,14 +189,15 @@
<Box className="mt-3 text-center"> <Box className="mt-3 text-center">
<div class="flex justify-between flex-col items-center w-[80%] md:w-96"> <div class="flex justify-between flex-col items-center w-[80%] md:w-96">
<img src="/images/empty-exercise-icon.svg" alt="Exercise" class="my-2.5 mx-auto" /> <img src="/images/empty-exercise-icon.svg" alt="Exercise" class="my-2.5 mx-auto" />
<h2 class="text-xl my-1.5 font-normal">No exercises added for this lesson</h2> <h2 class="text-xl my-1.5 font-normal">
{$t('course.navItem.lessons.exercises.no_exercises')}
</h2>
<p class="text-sm text-center text-slate-500"> <p class="text-sm text-center text-slate-500">
{#if isStudent} {#if isStudent}
Your tutor hasn't assigned any exercise to this lesson. For the main time, you can {$t('course.navItem.lessons.exercises.no_assigned_exercises')}
<strong>chill with the big boys :)</strong> <strong> {$t('course.navItem.lessons.exercises.chill')} :)</strong>
{:else} {:else}
Share your knowledge with the world by creating engaging exercises. Add an exercise {$t('course.navItem.lessons.exercises.add_exercise')}
now.
{/if} {/if}
</p> </p>
</div> </div>
@@ -0,0 +1,12 @@
<script lang="ts">
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
</script>
<div class="w-full">
<SkeletonPlaceholder style="width: 100%; height: 10rem;" />
<div class="my-3">
<SkeletonText style="width: 100%;" />
<SkeletonText style="width: 100%;" />
<SkeletonText style="width: 100%;" />
</div>
</div>
@@ -5,6 +5,7 @@
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import { lesson, isLessonDirty } from '$lib/components/Course/components/Lesson/store/lessons'; import { lesson, isLessonDirty } from '$lib/components/Course/components/Lesson/store/lessons';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { t } from '$lib/utils/functions/translations';
let genericLinks = ''; let genericLinks = '';
let error = ''; let error = '';
@@ -48,7 +49,7 @@
<div class="w-full flex items-{error ? 'center' : 'end'} justify-between gap-5"> <div class="w-full flex items-{error ? 'center' : 'end'} justify-between gap-5">
<TextField <TextField
label="Embed link" label={$t('course.navItem.lessons.materials.tabs.video.embed_link')}
bind:value={genericLinks} bind:value={genericLinks}
className="flex-1 text-left " className="flex-1 text-left "
inputClassName="text-sm" inputClassName="text-sm"
@@ -56,10 +57,15 @@
placeholder="https://www.videoplayer.com/" placeholder="https://www.videoplayer.com/"
errorMessage={error} errorMessage={error}
/> />
<PrimaryButton label="Add Video" className="rounded-md" onClick={addVideo} /> <PrimaryButton
label={$t('course.navItem.lessons.materials.tabs.video.add_video.add_video')}
className="rounded-md"
onClick={addVideo}
/>
</div> </div>
<p class="mt-4 pl-2 text-sm"> <p class="mt-4 pl-2 text-sm">
Videos added: <strong> {$t('course.navItem.lessons.materials.tabs.video.add_video.videos_added')}:
<strong>
{$lesson?.materials?.videos.filter((v) => v.type === 'generic').length || 0} {$lesson?.materials?.videos.filter((v) => v.type === 'generic').length || 0}
</strong> </strong>
</p> </p>
@@ -3,6 +3,7 @@
import LocalVideo from './LocalVideo.svelte'; import LocalVideo from './LocalVideo.svelte';
import EmbedLink from './EmbedLink.svelte'; import EmbedLink from './EmbedLink.svelte';
import * as CONSTANTS from './constants'; import * as CONSTANTS from './constants';
import { t } from '$lib/utils/functions/translations';
import { isFreePlan } from '$lib/utils/store/org'; import { isFreePlan } from '$lib/utils/store/org';
import FlashFilled from 'carbon-icons-svelte/lib/FlashFilled.svelte'; import FlashFilled from 'carbon-icons-svelte/lib/FlashFilled.svelte';
@@ -17,7 +18,7 @@
<section class="w-full h-full flex flex-col md:flex-row items-start gap-3"> <section class="w-full h-full flex flex-col md:flex-row items-start gap-3">
<div class="flex flex-row md:flex-col gap-2 items-center"> <div class="flex flex-row md:flex-col gap-2 items-center">
<p class="text-sm font-normal text-start text-[#4F4B4B] dark:text-[#b0a9a9] mb-3 w-full"> <p class="text-sm font-normal text-start text-[#4F4B4B] dark:text-[#b0a9a9] mb-3 w-full">
Add By {$t('course.navItem.lessons.materials.tabs.video.add_video.add_by')}
</p> </p>
{#each tabs as item (item.value)} {#each tabs as item (item.value)}
<button <button
@@ -37,7 +38,7 @@
color={`${currentTab === item.value ? 'dark:invert-0' : 'dark:invert'}`} color={`${currentTab === item.value ? 'dark:invert-0' : 'dark:invert'}`}
/> />
{/if} {/if}
<p>{item.title}</p> <p>{$t(item.title)}</p>
</button> </button>
{/each} {/each}
</div> </div>
@@ -18,6 +18,7 @@
let status = 'active'; let status = 'active';
let fileSize; let fileSize;
let isDisabled = false; let isDisabled = false;
import { t } from '$lib/utils/functions/translations';
export let lessonId = ''; export let lessonId = '';
@@ -122,7 +123,7 @@
</script> </script>
<UpgradeBanner className="mb-3" onClick={() => ($uploadCourseVideoStore.isModalOpen = false)}> <UpgradeBanner className="mb-3" onClick={() => ($uploadCourseVideoStore.isModalOpen = false)}>
Upgrade your plan to upload vidoes {$t('course.navItem.lessons.materials.tabs.video.add_video.upgrade')}
</UpgradeBanner> </UpgradeBanner>
{#if !isLoaded} {#if !isLoaded}
@@ -138,18 +139,22 @@
> >
{#if isLoading} {#if isLoading}
<div class="flex flex-col gap-5 max-w-[500px] w-[60%] justify-center"> <div class="flex flex-col gap-5 max-w-[500px] w-[60%] justify-center">
<p class="mt-5 text-center">Uploading...</p> <p class="mt-5 text-center">
{$t('course.navItem.lessons.materials.tabs.video.add_video.uploading')}
</p>
<ProgressBar class="w-full" {value} {max} {status} /> <ProgressBar class="w-full" {value} {max} {status} />
<p class="text-sm">{helperText}</p> <p class="text-sm">{helperText}</p>
</div> </div>
{:else} {:else}
<img src="/upload-video.svg" alt="upload" /> <img src="/upload-video.svg" alt="upload" />
<span class="pt-3"> <span class="pt-3">
<h3 class="text-center text-xl font-normal dark:text-white">Upload video</h3> <h3 class="text-center text-xl font-normal dark:text-white">
{$t('course.navItem.lessons.materials.tabs.video.add_video.upload_video')}
</h3>
<p class=" text-center text-sm font-normal"> <p class=" text-center text-sm font-normal">
Select file( Mp4, MOV, AVI) to upload to your lesson. {$t('course.navItem.lessons.materials.tabs.video.add_video.select_file')}
</p> </p>
<p>(Max 500 MB)</p> <p>{$t('course.navItem.lessons.materials.tabs.video.add_video.size')}</p>
</span> </span>
{/if} {/if}
<input <input
@@ -169,27 +174,34 @@
<img src="/video-upload-error.svg" alt="upload error" /> <img src="/video-upload-error.svg" alt="upload error" />
<span class="pb-2 pt-3"> <span class="pb-2 pt-3">
<h3 class="text-center text-base font-normal dark:text-white"> <h3 class="text-center text-base font-normal dark:text-white">
Oops 😬, couldn’t upload video {$t('course.navItem.lessons.materials.tabs.video.add_video.oops')}
</h3> </h3>
<p class="text-center text-xs font-normal text-[#ADADAD]"> <p class="text-center text-xs font-normal text-[#ADADAD]">
Sorry we video wasn’t uploaded. The file size is too big,<br /> maximum size is 30 MB. Try again! {$t('course.navItem.lessons.materials.tabs.video.add_video.sorry')}<br />
{$t('course.navItem.lessons.materials.tabs.video.add_video.sorry_2')}
</p> </p>
</span> </span>
<PrimaryButton label="Try another file" onClick={tryAgain} /> <PrimaryButton
label={$t('course.navItem.lessons.materials.tabs.video.add_video.button')}
onClick={tryAgain}
/>
</div> </div>
{:else if !formRes?.success} {:else if !formRes?.success}
<div class="flex h-full w-full flex-col items-center justify-center rounded-xl"> <div class="flex h-full w-full flex-col items-center justify-center rounded-xl">
<img src="/video-upload-error.svg" alt="upload error" /> <img src="/video-upload-error.svg" alt="upload error" />
<span class="pb-2 pt-3"> <span class="pb-2 pt-3">
<h3 class="text-center text-base font-normal dark:text-white"> <h3 class="text-center text-base font-normal dark:text-white">
Oops 😬, couldn’t upload video {$t('course.navItem.lessons.materials.tabs.video.add_video.oops')}
</h3> </h3>
<p class="text-center text-xs font-normal text-[#ADADAD]"> <p class="text-center text-xs font-normal text-[#ADADAD]">
Sorry we video wasn’t uploaded, the file format isn’t supported or something went wrong on {$t('course.navItem.lessons.materials.tabs.video.add_video.sorry_3')}<br />
the server.<br /> Upload MP4, MOV and AVI files. {$t('course.navItem.lessons.materials.tabs.video.add_video.sorry_4')}
</p> </p>
</span> </span>
<PrimaryButton label="Please try again" onClick={tryAgain} /> <PrimaryButton
label={$t('course.navItem.lessons.materials.tabs.video.add_video.try_again')}
onClick={tryAgain}
/>
</div> </div>
{:else} {:else}
<div class="flex h-full w-full flex-col items-start justify-between"> <div class="flex h-full w-full flex-col items-start justify-between">
@@ -210,7 +222,10 @@
<img src="/delete-video.svg" alt="deletevideo" class="dark:invert" /> <img src="/delete-video.svg" alt="deletevideo" class="dark:invert" />
</button> </button>
</div> </div>
<PrimaryButton label="Done" onClick={() => ($uploadCourseVideoStore.isModalOpen = false)} /> <PrimaryButton
label={$t('course.navItem.lessons.materials.button_done')}
onClick={() => ($uploadCourseVideoStore.isModalOpen = false)}
/>
</div> </div>
</div> </div>
{/if} {/if}
@@ -5,6 +5,7 @@
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import { lesson, isLessonDirty } from '$lib/components/Course/components/Lesson/store/lessons'; import { lesson, isLessonDirty } from '$lib/components/Course/components/Lesson/store/lessons';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { t } from '$lib/utils/functions/translations';
let youtubeLinks = ''; let youtubeLinks = '';
let error = ''; let error = '';
@@ -49,7 +50,7 @@
<div class="w-full flex items-{error ? 'center' : 'end'} justify-between gap-5"> <div class="w-full flex items-{error ? 'center' : 'end'} justify-between gap-5">
<TextField <TextField
label="Youtube link" label={$t('course.navItem.lessons.materials.tabs.video.add_video.youtube_link')}
bind:value={youtubeLinks} bind:value={youtubeLinks}
className="flex-1 text-left " className="flex-1 text-left "
inputClassName="text-sm" inputClassName="text-sm"
@@ -57,10 +58,15 @@
placeholder="https://www.youtube.com/watch?v=" placeholder="https://www.youtube.com/watch?v="
errorMessage={error} errorMessage={error}
/> />
<PrimaryButton label="Add Video" className="rounded-md" onClick={addVideo} /> <PrimaryButton
label={$t('course.navItem.lessons.materials.tabs.video.add_video.add_video')}
className="rounded-md"
onClick={addVideo}
/>
</div> </div>
<p class="mt-4 pl-2 text-sm"> <p class="mt-4 pl-2 text-sm">
Videos added: <strong> {$t('course.navItem.lessons.materials.tabs.video.add_video.videos_added')}:
<strong>
{$lesson?.materials?.videos.filter((v) => v.type === 'youtube' && isValidYouTubeLink(v.link)) {$lesson?.materials?.videos.filter((v) => v.type === 'youtube' && isValidYouTubeLink(v.link))
.length || 0} .length || 0}
</strong> </strong>
@@ -1,21 +1,22 @@
import Link from 'carbon-icons-svelte/lib/Link.svelte'; import Link from 'carbon-icons-svelte/lib/Link.svelte';
import Video from 'carbon-icons-svelte/lib/Video.svelte'; import Video from 'carbon-icons-svelte/lib/Video.svelte';
import DirectLink from 'carbon-icons-svelte/lib/DirectLink.svelte'; import DirectLink from 'carbon-icons-svelte/lib/DirectLink.svelte';
export const videoTabs = [ export const videoTabs = [
{ {
value: 1, value: 1,
title: 'Youtube Link', title: 'course.navItem.lessons.materials.tabs.video.add_video.youtube_link',
icon: Link icon: Link
}, },
{ {
value: 2, value: 2,
title: 'Embed Link', title: 'course.navItem.lessons.materials.tabs.video.embed_link',
icon: DirectLink icon: DirectLink
}, },
{ {
value: 3, value: 3,
title: 'Upload', title: 'course.navItem.lessons.materials.tabs.video.upload',
icon: Video icon: Video
} }
]; ];
@@ -1,15 +1,37 @@
<script lang="ts"> <script lang="ts">
import { lesson } from '$lib/components/Course/components/Lesson/store/lessons';
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte'; import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
import { isHtmlValueEmpty } from '$lib/utils/functions/toHtml'; import { isHtmlValueEmpty } from '$lib/utils/functions/toHtml';
import {
lesson,
lessonByTranslation
} from '$lib/components/Course/components/Lesson/store/lessons';
import Box from '$lib/components/Box/index.svelte';
import { t } from '$lib/utils/functions/translations';
import AlignBoxTopLeft from 'carbon-icons-svelte/lib/AlignBoxTopLeft.svelte';
import { lessonFallbackNote } from '$lib/utils/functions/translations';
export let lessonId: string;
$: content = lessonFallbackNote(
$lesson.materials.note,
$lessonByTranslation[lessonId],
$lesson.locale
);
</script> </script>
{#if !isHtmlValueEmpty($lesson.materials?.note)} {#if !isHtmlValueEmpty(content)}
<HtmlRender className="m-auto"> <HtmlRender className="m-auto">
<svelte:fragment slot="content"> <svelte:fragment slot="content">
<div> <div>
{@html $lesson.materials.note} {@html content}
</div> </div>
</svelte:fragment> </svelte:fragment>
</HtmlRender> </HtmlRender>
{:else}
<Box className="text-center">
<AlignBoxTopLeft class="carbon-icon" size={32} />
<h3 class="text-xl font-normal dark:text-white py-2">
{$t('course.navItem.lessons.materials.no_translation')}
</h3>
</Box>
{/if} {/if}
@@ -2,33 +2,33 @@ import { Video, Notebook, PresentationFile } from 'carbon-icons-svelte';
export let tabs = [ export let tabs = [
{ {
label: 'Note', label: 'course.navItem.lessons.materials.tabs.note.title',
icon: Notebook, icon: Notebook,
value: 1, value: 1,
badgeValue: 0 badgeValue: 0
}, },
{ {
label: 'Slide', label: 'course.navItem.lessons.materials.tabs.slide.title',
icon: PresentationFile, icon: PresentationFile,
value: 2, value: 2,
badgeValue: 0 badgeValue: 0
}, },
{ {
label: 'Video', label: 'course.navItem.lessons.materials.tabs.video.title',
icon: Video, icon: Video,
value: 3, value: 3,
badgeValue: 0 badgeValue: 0
} }
]; ];
export function orderedTabs(tabs, Settingtabs) { export function orderedTabs(tabs, settingTabs) {
if (!Array.isArray(Settingtabs)) return tabs; if (!Array.isArray(settingTabs)) return tabs;
const reorderedTabs = []; const reorderedTabs = [];
const tabMap = new Map(tabs.map((tab) => [tab.label, tab])); const tabMap = new Map(tabs.map((tab) => [tab.value, tab]));
Settingtabs.forEach((settingTab, index) => { settingTabs.forEach((settingTab, index) => {
const tab = tabMap.get(settingTab.name); const tab = tabMap.get(settingTab.id);
if (tab) { if (tab) {
reorderedTabs[index] = tab; reorderedTabs[index] = tab;
} }
@@ -20,6 +20,7 @@
import { import {
lesson, lesson,
lessons, lessons,
lessonByTranslation,
handleUpdateLessonMaterials, handleUpdateLessonMaterials,
isLessonDirty, isLessonDirty,
uploadCourseVideoStore, uploadCourseVideoStore,
@@ -37,6 +38,10 @@
import type { LessonPage } from '$lib/utils/types'; import type { LessonPage } from '$lib/utils/types';
import { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
import { isHtmlValueEmpty } from '$lib/utils/functions/toHtml'; import { isHtmlValueEmpty } from '$lib/utils/functions/toHtml';
import { t, lessonFallbackNote } from '$lib/utils/functions/translations';
import { supabase } from '$lib/utils/functions/supabase';
import type { LOCALE } from '$lib/utils/types';
import Loader from './Loader.svelte';
export let mode = MODES.view; export let mode = MODES.view;
export let prevMode = ''; export let prevMode = '';
@@ -45,7 +50,9 @@
export let isStudent = false; export let isStudent = false;
export let toggleMode = () => {}; export let toggleMode = () => {};
let localeExists: Record<string, boolean> = {};
let lessonTitle = ''; let lessonTitle = '';
let prevContent = '';
let initAutoSave = false; let initAutoSave = false;
let timeoutId: NodeJS.Timeout; let timeoutId: NodeJS.Timeout;
let tabs = CONSTANTS.tabs; let tabs = CONSTANTS.tabs;
@@ -61,15 +68,60 @@
let aiButtonClass = let aiButtonClass =
'flex items-center px-5 py-2 border border-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md w-full mb-2'; 'flex items-center px-5 py-2 border border-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md w-full mb-2';
const onChange = const onChange = (tab) => {
(tab = 0) => return () => {
() => currentTab = tab;
(currentTab = tab); };
};
const getValue = (label: string) => { const getValue = (label: string) => {
const tabValue = tabs.find((tab) => tab.label === label)?.value; const tabValue = tabs.find((tab) => tab.label === label)?.value;
return tabValue; return tabValue;
}; };
async function saveOrUpdateTranslation(locale, lessonId) {
const content = $lessonByTranslation[lessonId][locale];
if (typeof localeExists[locale] === 'undefined') {
console.log('find the language');
const { data } = await supabase
.from('lesson_language')
.select(`id`)
.eq('lesson_id', lessonId)
.eq('locale', locale)
.maybeSingle();
localeExists[locale] = !!(data && data?.id);
}
if (localeExists[locale]) {
const { error: updateError } = await supabase
.from('lesson_language')
.update({ content })
.eq('lesson_id', lessonId)
.eq('locale', locale);
if (updateError) {
console.error('Error updating translation:', updateError.message);
snackbar.error('Updating translations failed');
}
} else {
const { error: insertError } = await supabase.from('lesson_language').insert({
locale,
lesson_id: lessonId,
content
});
if (insertError) {
console.error('Error inserting translation:', insertError.message);
snackbar.error('Creating new translations failed');
return;
}
localeExists[locale] = true;
}
}
async function saveLesson(materials?: LessonPage['materials']) { async function saveLesson(materials?: LessonPage['materials']) {
const _lesson = !!materials const _lesson = !!materials
? { ? {
@@ -77,13 +129,30 @@
materials materials
} }
: $lesson; : $lesson;
return handleUpdateLessonMaterials(_lesson, lessonId);
const [lessonRes] = await Promise.all([
handleUpdateLessonMaterials(_lesson, lessonId),
saveOrUpdateTranslation($lesson.locale, lessonId)
]);
return lessonRes;
} }
function isMaterialsEmpty(materials: LessonPage['materials']) { function isMaterialsEmpty(
materials: LessonPage['materials'],
translation: Record<LOCALE, string>
) {
console.log('isMaterialsEmpty', {
translation
});
const { slide_url, videos, note } = materials; const { slide_url, videos, note } = materials;
return isHtmlValueEmpty(note) && !slide_url && isEmpty(videos); return (
isHtmlValueEmpty(note) &&
!slide_url &&
isEmpty(videos) &&
Object.values(translation || {}).every((t) => isHtmlValueEmpty(t))
);
} }
function handleSave(prevMode: string) { function handleSave(prevMode: string) {
@@ -119,9 +188,13 @@
}); });
function updateNoteByCompletion(completion: string) { function updateNoteByCompletion(completion: string) {
$lesson.materials.note = completion; if (!completion) return;
autoSave($lesson.materials); if ($lessonByTranslation[lessonId]) {
$lessonByTranslation[lessonId][$lesson.locale] = `${prevContent}${completion}`;
}
autoSave($lesson.materials, $lessonByTranslation[lessonId], false, lessonId);
if (editorWindowRef) { if (editorWindowRef) {
const tmceBody = editorWindowRef?.document?.querySelector('body'); const tmceBody = editorWindowRef?.document?.querySelector('body');
@@ -132,12 +205,16 @@
} }
function callAI(type = '') { function callAI(type = '') {
prevContent = $lessonByTranslation[lessonId]?.[$lesson.locale] || '';
const _lesson = $lessons.find((les) => les.id === $lesson.id); const _lesson = $lessons.find((les) => les.id === $lesson.id);
$input = JSON.stringify({ $input = JSON.stringify({
type, type,
lessonTitle: _lesson?.title || '', lessonTitle: _lesson?.title || '',
courseTitle: $course.title courseTitle: $course.title,
locale: $lesson.locale
}); });
setTimeout(() => { setTimeout(() => {
handleSubmit({ preventDefault: () => {} }); handleSubmit({ preventDefault: () => {} });
}, 500); }, 500);
@@ -157,14 +234,14 @@
function autoSave( function autoSave(
updatedMaterials: LessonPage['materials'], updatedMaterials: LessonPage['materials'],
translation: Record<LOCALE, string>,
_isLoading?: boolean, _isLoading?: boolean,
lessonId?: string lessonId?: string
) { ) {
// don't autosave on view mode
if (mode === MODES.view) return; if (mode === MODES.view) return;
if (timeoutId) clearTimeout(timeoutId); if (timeoutId) clearTimeout(timeoutId);
console.log('autosaving');
if (!initAutoSave) { if (!initAutoSave) {
initAutoSave = true; initAutoSave = true;
return; return;
@@ -172,21 +249,18 @@
isSaving = true; isSaving = true;
timeoutId = setTimeout(async () => { timeoutId = setTimeout(async () => {
console.log('autosaving');
const { error } = await saveLesson(updatedMaterials); const { error } = await saveLesson(updatedMaterials);
if (error) { if (error) {
console.log('error saving lesson', error); console.log('error saving lesson', error);
snackbar.error('We apologise, there was an error saving your lesson.'); snackbar.error('snackbar.materials.apology');
} }
isSaving = false; isSaving = false;
}, 500); }, 1000);
} }
function handleInputChange() { async function onLessonIdChange(_lid: string) {
$isLessonDirty = true;
}
function onLessonIdChange(_lid: string) {
initAutoSave = false; initAutoSave = false;
isSaving = false; isSaving = false;
@@ -201,15 +275,15 @@
function getComponentOrder(tabs = CONSTANTS.tabs) { function getComponentOrder(tabs = CONSTANTS.tabs) {
const componentMap = { const componentMap = {
Video: ComponentVideo, '1': ComponentNote,
Slide: ComponentSlide, '2': ComponentSlide,
Note: ComponentNote '3': ComponentVideo
}; };
const componentNames = tabs const componentNames = tabs
.map((tab) => { .map((tab) => {
// @ts-ignore // @ts-ignore
const component = componentMap[tab.label]; const component = componentMap[tab.value];
return component || null; return component || null;
}) })
.filter(Boolean); .filter(Boolean);
@@ -217,7 +291,7 @@
return componentNames; return componentNames;
} }
$: autoSave($lesson.materials, $isLoading, lessonId); $: autoSave($lesson.materials, $lessonByTranslation[lessonId], $isLoading, lessonId);
$: onLessonIdChange(lessonId); $: onLessonIdChange(lessonId);
@@ -225,20 +299,24 @@
$: addBadgeValueToTab($lesson.materials); $: addBadgeValueToTab($lesson.materials);
$: console.log('$lesson.materials', $lesson.materials);
$: updateNoteByCompletion($completion); $: updateNoteByCompletion($completion);
$: initPlyr(player, $lesson.materials.videos); $: initPlyr(player, $lesson.materials.videos);
$: lessonTitle = $lessons.find((les) => les.id === $lesson.id)?.title || ''; $: lessonTitle = $lessons.find((les) => les.id === $lesson.id)?.title || '';
$: editorValue = lessonFallbackNote(
$lesson.materials.note,
$lessonByTranslation[lessonId],
$lesson.locale
);
</script> </script>
<Modal <Modal
{onClose} {onClose}
bind:open={$uploadCourseVideoStore.isModalOpen} bind:open={$uploadCourseVideoStore.isModalOpen}
width="w-4/5 w-[90%] h-[80%] md:h-[566px]" width="w-4/5 w-[90%] h-[80%] md:h-[566px]"
modalHeading="Add a Video" modalHeading={$t('course.navItem.lessons.materials.tabs.video.add_video.title')}
> >
<VideoUploader {lessonId} /> <VideoUploader {lessonId} />
</Modal> </Modal>
@@ -251,72 +329,93 @@
</svelte:fragment> </svelte:fragment>
</HtmlRender> </HtmlRender>
{#if mode === MODES.edit} {#if $lesson.isFetching}
<Loader />
{:else if mode === MODES.edit}
<Tabs {tabs} {currentTab} {onChange}> <Tabs {tabs} {currentTab} {onChange}>
<slot:fragment slot="content"> <slot:fragment slot="content">
<TabContent value={getValue('Note')} index={currentTab}> <TabContent
<div bind:this={aiButtonRef} class="w-full flex flex-row-reverse"> value={getValue('course.navItem.lessons.materials.tabs.note.title')}
<PrimaryButton index={currentTab}
className="flex items-center relative" >
onClick={() => { <div class="flex gap-1 justify-end">
openPopover = !openPopover; <div bind:this={aiButtonRef} class="flex flex-row-reverse">
}} <PrimaryButton
isLoading={$isLoading} className="flex items-center relative"
isDisabled={$isLoading} onClick={() => {
variant={VARIANTS.OUTLINED} openPopover = !openPopover;
disableScale
>
<MagicWandFilled size={20} class="carbon-icon mr-3" />
AI
<Popover
caret
align="left"
bind:open={openPopover}
on:click:outside={({ detail }) => {
openPopover = aiButtonRef?.contains(detail.target);
}} }}
isLoading={$isLoading}
isDisabled={$isLoading}
variant={VARIANTS.OUTLINED}
disableScale
> >
<div class="p-2"> <MagicWandFilled size={20} class="carbon-icon mr-3" />
<button class={aiButtonClass} on:click={() => callAI('outline')}> AI
<ListIcon class="carbon-icon mr-2" /> <Popover
Generate Lesson Outline caret
</button> align="left"
<button class={aiButtonClass} on:click={() => callAI('note')}> bind:open={openPopover}
<AlignBoxTopLeftIcon class="carbon-icon mr-2" /> on:click:outside={({ detail }) => {
Generate Lesson Note openPopover = aiButtonRef?.contains(detail.target);
</button> }}
<button class={aiButtonClass} on:click={() => callAI('activities')}> >
<IbmWatsonKnowledgeStudioIcon class="carbon-icon mr-2" /> <div class="p-2">
Generate Lesson Activities <button class={aiButtonClass} on:click={() => callAI('outline')}>
</button> <ListIcon class="carbon-icon mr-2" />
</div> {$t('course.navItem.lessons.materials.tabs.note.ai.outline')}
</Popover> </button>
</PrimaryButton> <button class={aiButtonClass} on:click={() => callAI('note')}>
<AlignBoxTopLeftIcon class="carbon-icon mr-2" />
{$t('course.navItem.lessons.materials.tabs.note.ai.note')}
</button>
<button class={aiButtonClass} on:click={() => callAI('activities')}>
<IbmWatsonKnowledgeStudioIcon class="carbon-icon mr-2" />
{$t('course.navItem.lessons.materials.tabs.note.ai.activities')}
</button>
</div>
</Popover>
</PrimaryButton>
</div>
</div> </div>
<div class="h-[60vh] mt-5"> <div class="h-[60vh] mt-5">
<TextEditor <TextEditor
id={lessonId} id={lessonId}
bind:editorWindowRef bind:editorWindowRef
value={$lesson.materials.note} value={editorValue}
onChange={(html) => ($lesson.materials.note = html)} onChange={(html) => {
placeholder="Write your lesson note here" if (mode === MODES.view) return;
$lessonByTranslation[lessonId][$lesson.locale] = html;
$isLessonDirty = true;
}}
placeholder={$t('course.navItem.lessons.materials.tabs.note.placeholder')}
/> />
</div> </div>
</TabContent> </TabContent>
<TabContent value={getValue('Slide')} index={currentTab}> <TabContent
value={getValue('course.navItem.lessons.materials.tabs.slide.title')}
index={currentTab}
>
{#if mode === MODES.edit} {#if mode === MODES.edit}
<TextField <TextField
label="Slide link" label={$t('course.navItem.lessons.materials.tabs.slide.slide_link')}
bind:value={$lesson.materials.slide_url} bind:value={$lesson.materials.slide_url}
onInputChange={handleInputChange} onInputChange={() => ($isLessonDirty = true)}
helperMessage="You can embed Google Slides or Canva Presentation" helperMessage={$t('course.navItem.lessons.materials.tabs.slide.helper_message')}
/> />
{/if} {/if}
</TabContent> </TabContent>
<TabContent value={getValue('Video')} index={currentTab}> <TabContent
<PrimaryButton label="Add/Edit Video(s)" onClick={openAddVideoModal} className="mb-2" /> value={getValue('course.navItem.lessons.materials.tabs.video.title')}
index={currentTab}
>
<PrimaryButton
label={$t('course.navItem.lessons.materials.tabs.video.button')}
onClick={openAddVideoModal}
className="mb-2"
/>
{#if $lesson.materials.videos.length} {#if $lesson.materials.videos.length}
<div class="flex flex-col items-start w-full h-full"> <div class="flex flex-col items-start w-full h-full">
{#each $lesson.materials.videos as video, index} {#each $lesson.materials.videos as video, index}
@@ -382,24 +481,30 @@
</TabContent> </TabContent>
</slot:fragment> </slot:fragment>
</Tabs> </Tabs>
{:else if !isMaterialsEmpty($lesson.materials)} {:else if !isMaterialsEmpty($lesson.materials, $lessonByTranslation[lessonId])}
<div class="w-full"> <div class="w-full">
{#each componentsToRender as Component} {#each componentsToRender as Component}
<svelte:component this={Component} /> <svelte:component this={Component} {lessonId} />
{/each} {/each}
</div> </div>
{:else} {:else}
<Box className="text-center"> <Box className="text-center">
<img src="/no-video.svg" alt="Video not found" /> <img src="/no-video.svg" alt="Video not found" />
<h3 class="text-xl font-normal dark:text-white py-2"> <h3 class="text-xl font-normal dark:text-white py-2">
No note, video or slide added for this lesson yet {$t('course.navItem.lessons.materials.body_heading')}
</h3> </h3>
{#if !isStudent} {#if !isStudent}
<p class="text-sm text-center font-normal py-2"> <p class="text-sm text-center font-normal py-2">
Share your knowledge with your students by creating engaging content<br /> {$t('course.navItem.lessons.materials.body_content')}
Start by clicking on <strong>Get started</strong> button. <strong>{$t('course.navItem.lessons.materials.get_started')}</strong>
{$t('course.navItem.lessons.materials.button')}.
</p> </p>
<PrimaryButton label="Get started" className="rounded-md" onClick={toggleMode} /> <PrimaryButton
label={$t('course.navItem.lessons.materials.get_started')}
className="rounded-md"
onClick={toggleMode}
/>
{/if} {/if}
</Box> </Box>
{/if} {/if}
@@ -2,6 +2,7 @@ import { writable } from 'svelte/store';
import type { Writable, Updater } from 'svelte/store'; import type { Writable, Updater } from 'svelte/store';
import { createLesson, updateLesson, deleteLesson } from '$lib/utils/services/courses'; import { createLesson, updateLesson, deleteLesson } from '$lib/utils/services/courses';
import type { Lesson, Course, LessonPage, LessonComment } from '$lib/utils/types'; import type { Lesson, Course, LessonPage, LessonComment } from '$lib/utils/types';
import { LOCALE } from '$lib/utils/types';
import { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
export const uploadCourseVideoStore = writable({ export const uploadCourseVideoStore = writable({
@@ -12,9 +13,11 @@ export const lessons: Writable<Lesson[]> = writable([]);
export const lesson = writable<LessonPage>({ export const lesson = writable<LessonPage>({
id: null, id: null,
locale: LOCALE.EN,
totalExercises: 0, totalExercises: 0,
totalComments: 0, totalComments: 0,
isSaving: false, isSaving: false,
isFetching: false,
materials: { materials: {
note: '', note: '',
slide_url: '', slide_url: '',
@@ -24,7 +27,11 @@ export const lesson = writable<LessonPage>({
lesson_completion: [] lesson_completion: []
}); });
export let lessonComments = writable<LessonComment[]>([]); export const lessonByTranslation = writable<{
[key: string]: Record<LOCALE, string>;
}>({});
export const lessonComments = writable<LessonComment[]>([]);
export const isLessonDirty = writable(false); export const isLessonDirty = writable(false);
@@ -16,6 +16,7 @@
import { profile } from '$lib/utils/store/user'; import { profile } from '$lib/utils/store/user';
import { getIsLessonComplete } from '../Lesson/functions'; import { getIsLessonComplete } from '../Lesson/functions';
import { isFreePlan } from '$lib/utils/store/org'; import { isFreePlan } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations';
export let path: string; export let path: string;
export let isStudent: boolean = false; export let isStudent: boolean = false;
@@ -137,13 +138,13 @@
$: { $: {
navItems = [ navItems = [
{ {
label: 'News Feed', label: $t('course.navItems.1'),
to: getNavItemRoute($course.id), to: getNavItemRoute($course.id),
hideSortIcon: true, hideSortIcon: true,
isPaidFeature: false isPaidFeature: false
}, },
{ {
label: 'Lessons', label: $t('course.navItems.2'),
to: getLessonsRoute($course.id), to: getLessonsRoute($course.id),
hideSortIcon: false, hideSortIcon: false,
isPaidFeature: false, isPaidFeature: false,
@@ -151,13 +152,13 @@
isExpanded: isStudent ? true : $page.url.pathname.includes('/lessons') isExpanded: isStudent ? true : $page.url.pathname.includes('/lessons')
}, },
{ {
label: 'Attendance', label: $t('course.navItems.3'),
to: getNavItemRoute($course.id, 'attendance'), to: getNavItemRoute($course.id, 'attendance'),
isPaidFeature: false, isPaidFeature: false,
hideSortIcon: true hideSortIcon: true
}, },
{ {
label: 'Submissions', label: $t('course.navItems.4'),
to: getNavItemRoute($course.id, 'submissions'), to: getNavItemRoute($course.id, 'submissions'),
hideSortIcon: true, hideSortIcon: true,
isPaidFeature: false, isPaidFeature: false,
@@ -166,13 +167,13 @@
} }
}, },
{ {
label: 'Marks', label: $t('course.navItems.5'),
to: getNavItemRoute($course.id, 'marks'), to: getNavItemRoute($course.id, 'marks'),
isPaidFeature: false, isPaidFeature: false,
hideSortIcon: true hideSortIcon: true
}, },
{ {
label: 'People', label: $t('course.navItems.6'),
to: getNavItemRoute($course.id, 'people'), to: getNavItemRoute($course.id, 'people'),
isPaidFeature: false, isPaidFeature: false,
hideSortIcon: true, hideSortIcon: true,
@@ -181,7 +182,7 @@
} }
}, },
{ {
label: 'Certificates', label: $t('course.navItems.7'),
to: getNavItemRoute($course.id, 'certificates'), to: getNavItemRoute($course.id, 'certificates'),
hideSortIcon: true, hideSortIcon: true,
isPaidFeature: true, isPaidFeature: true,
@@ -194,7 +195,7 @@
} }
}, },
{ {
label: 'Landing Page', label: $t('course.navItems.8'),
to: getNavItemRoute($course.id, 'landingpage'), to: getNavItemRoute($course.id, 'landingpage'),
hideSortIcon: true, hideSortIcon: true,
isPaidFeature: false, isPaidFeature: false,
@@ -203,7 +204,7 @@
} }
}, },
{ {
label: 'Settings', label: $t('course.navItems.9'),
to: getNavItemRoute($course.id, 'settings'), to: getNavItemRoute($course.id, 'settings'),
hideSortIcon: true, hideSortIcon: true,
isPaidFeature: false, isPaidFeature: false,
@@ -11,6 +11,7 @@
NOTIFICATION_NAME, NOTIFICATION_NAME,
triggerSendEmail triggerSendEmail
} from '$lib/utils/services/notification/notification'; } from '$lib/utils/services/notification/notification';
import { t } from '$lib/utils/functions/translations';
import { createNewsfeedValidation } from '$lib/utils/functions/validator'; import { createNewsfeedValidation } from '$lib/utils/functions/validator';
import { getTextFromHTML } from '$lib/utils/functions/toHtml'; import { getTextFromHTML } from '$lib/utils/functions/toHtml';
@@ -36,7 +37,7 @@
try { try {
if (edit) { if (edit) {
onEdit(editFeed.id, newPost); onEdit(editFeed.id, newPost);
snackbar.success('Feed edited successfully'); snackbar.success('snackbar.newsfeed.success.edit');
edit = false; edit = false;
newPost = ''; newPost = '';
@@ -74,7 +75,7 @@
isPinned: false isPinned: false
}); });
snackbar.success('New feed added successfully'); snackbar.success('snackbar.newsfeed.success.add');
triggerSendEmail(NOTIFICATION_NAME.NEWSFEED, { triggerSendEmail(NOTIFICATION_NAME.NEWSFEED, {
authorId: createdFeed.author_id, authorId: createdFeed.author_id,
feedId: createdFeed.id feedId: createdFeed.id
@@ -82,7 +83,10 @@
resetEditor(); resetEditor();
} }
} catch (error) { } catch (error) {
snackbar.error('An error occurred while ' + (edit ? 'editing feed' : 'creating feed')); snackbar.error(
'snackbar.newsfeed.error.error ' +
(edit ? 'snackbar.newsfeed.error.editing' : 'snackbar.newsfeed.error.creating')
);
} finally { } finally {
isLoading = false; isLoading = false;
} }
@@ -101,7 +105,9 @@
bind:open={$isNewFeedModal.open} bind:open={$isNewFeedModal.open}
width="w-4/5" width="w-4/5"
maxWidth="max-w-lg" maxWidth="max-w-lg"
modalHeading={edit === true ? 'Edit Post' : 'Make a Post'} modalHeading={edit === true
? $t('course.navItem.news_feed.heading_button.edit_post')
: $t('course.navItem.news_feed.heading_button.make_a_post')}
> >
<section class="flex flex-col rounded-xl pb-3 h-full w-2/"> <section class="flex flex-col rounded-xl pb-3 h-full w-2/">
<TextEditor <TextEditor
@@ -109,7 +115,7 @@
onChange={(text) => { onChange={(text) => {
newPost = text; newPost = text;
}} }}
placeholder="Share an update with your students" placeholder={$t('course.navItem.news_feed.heading_button.placeholder')}
maxHeight={400} maxHeight={400}
/> />
{#if errors.newPost} {#if errors.newPost}
@@ -117,8 +123,16 @@
{/if} {/if}
<div class="flex items-center justify-end py-2"> <div class="flex items-center justify-end py-2">
<div class="flex gap-2"> <div class="flex gap-2">
<PrimaryButton label="Cancel" variant={VARIANTS.OUTLINED} onClick={resetEditor} /> <PrimaryButton
<PrimaryButton {isLoading} label="Post" onClick={onPost} /> label={$t('course.navItem.news_feed.heading_button.cancel')}
variant={VARIANTS.OUTLINED}
onClick={resetEditor}
/>
<PrimaryButton
{isLoading}
label={$t('course.navItem.news_feed.heading_button.post')}
onClick={onPost}
/>
</div> </div>
</div> </div>
</section> </section>
@@ -4,6 +4,7 @@
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { deleteMemberModal } from './store'; import { deleteMemberModal } from './store';
import { t } from '$lib/utils/functions/translations';
export let email; export let email;
export let deletePerson = () => {}; export let deletePerson = () => {};
@@ -18,24 +19,24 @@
onClose={() => ($deleteMemberModal.open = false)} onClose={() => ($deleteMemberModal.open = false)}
bind:open={$deleteMemberModal.open} bind:open={$deleteMemberModal.open}
width="w-96" width="w-96"
modalHeading="Delete" modalHeading={$t('course.navItem.people.delete_confirmation.title')}
> >
<div> <div>
<h1 class="dark:text-white text-lg"> <h1 class="dark:text-white text-lg">
Are you sure you want to delete <strong>{email}</strong>? {$t('course.navItem.people.delete_confirmation.sure')} <strong>{email}</strong>?
</h1> </h1>
<div class="mt-5 flex items-center justify-between"> <div class="mt-5 flex items-center justify-between">
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="No" label={$t('course.navItem.people.delete_confirmation.no')}
onClick={() => ($deleteMemberModal.open = false)} onClick={() => ($deleteMemberModal.open = false)}
/> />
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="Yes" label={$t('course.navItem.people.delete_confirmation.yes')}
onClick={handleDelete} onClick={handleDelete}
/> />
</div> </div>
@@ -20,6 +20,7 @@
NOTIFICATION_NAME NOTIFICATION_NAME
} from '$lib/utils/services/notification/notification'; } from '$lib/utils/services/notification/notification';
import { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
import { t } from '$lib/utils/functions/translations';
interface Tutor { interface Tutor {
id: number; id: number;
@@ -117,8 +118,8 @@
function copyLink() { function copyLink() {
if (!$currentOrgDomain) { if (!$currentOrgDomain) {
snackbar.error('Org data missing, please reload the page'); snackbar.error('snackbar.people.error.missing_data');
console.error('No current org domain'); console.error('snackbar.people.error.no');
return; return;
} }
@@ -149,14 +150,14 @@
open={addPeopleParm === 'true'} open={addPeopleParm === 'true'}
width="w-4/5 md:w-2/5" width="w-4/5 md:w-2/5"
maxWidth="max-w-lg" maxWidth="max-w-lg"
modalHeading="Invite people" modalHeading={$t('course.navItem.people.invite_modal.title')}
> >
<form on:submit|preventDefault={onSubmit}> <form on:submit|preventDefault={onSubmit}>
<div class="mb-8"> <div class="mb-8">
<p class="text-base mb-1 font-semibold">Invite Tutors</p> <p class="text-base mb-1 font-semibold">{$t('course.navItem.people.invite_modal.invite')}</p>
<MultiSelect <MultiSelect
disabled={isLoadingTutors} disabled={isLoadingTutors}
label="Select tutors in organization..." label={$t('course.navItem.people.invite_modal.select')}
bind:selectedIds bind:selectedIds
items={tutors} items={tutors}
/> />
@@ -166,12 +167,12 @@
</span> </span>
{:else} {:else}
<span> <span>
To add tutor to your organization, {$t('course.navItem.people.invite_modal.to_add')}
<a <a
href={`/org/${$currentOrg.siteName}/settings/teams`} href={`/org/${$currentOrg.siteName}/settings/teams`}
class="underline text-primary-600" class="underline text-primary-600"
> >
go to organization settings {$t('course.navItem.people.invite_modal.go_to')}
</a> </a>
</span> </span>
{/if} {/if}
@@ -179,8 +180,10 @@
<div class="mb-8 w-[90%] flex justify-between items-center"> <div class="mb-8 w-[90%] flex justify-between items-center">
<div class="w-3/5"> <div class="w-3/5">
<p class="text-base mb-1 font-semibold">Invite Students</p> <p class="text-base mb-1 font-semibold">
<p class=" text-sm">You can invite students via an invite link</p> {$t('course.navItem.people.invite_modal.invite_students')}
</p>
<p class=" text-sm">{$t('course.navItem.people.invite_modal.via_link')}</p>
</div> </div>
<div class="relative"> <div class="relative">
@@ -191,16 +194,20 @@
onClick={copyLink} onClick={copyLink}
> >
<Link size={16} class="mr-1" /> <Link size={16} class="mr-1" />
Copy link {$t('course.navItem.people.invite_modal.copy_link')}
</PrimaryButton> </PrimaryButton>
<Popover caret open={copied} align="bottom"> <Popover caret open={copied} align="bottom">
<div style="padding: 5px">Copied Successfully</div> <div style="padding: 5px">{$t('course.navItem.people.invite_modal.success')}</div>
</Popover> </Popover>
</div> </div>
</div> </div>
<div class="mt-5 flex items-center flex-row-reverse"> <div class="mt-5 flex items-center flex-row-reverse">
<PrimaryButton className="px-6 py-3" label="Finish" type="submit" /> <PrimaryButton
className="px-6 py-3"
label={$t('course.navItem.people.invite_modal.finish')}
type="submit"
/>
</div> </div>
</form> </form>
</Modal> </Modal>
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { CodeSnippet, Grid, Row, Column, Toggle } from 'carbon-components-svelte'; import { CodeSnippet, Grid, Row, Column, Toggle } from 'carbon-components-svelte';
import { Restart } from 'carbon-icons-svelte';
import { PUBLIC_SERVER_URL } from '$env/static/public'; import { PUBLIC_SERVER_URL } from '$env/static/public';
import SectionTitle from '$lib/components/Org/SectionTitle.svelte'; import SectionTitle from '$lib/components/Org/SectionTitle.svelte';
@@ -22,10 +23,9 @@
import UploadWidget from '$lib/components/UploadWidget/index.svelte'; import UploadWidget from '$lib/components/UploadWidget/index.svelte';
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store'; import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte'; import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
import { Restart } from 'carbon-icons-svelte';
import IconButton from '$lib/components/IconButton/index.svelte'; import IconButton from '$lib/components/IconButton/index.svelte';
import generateSlug from '$lib/utils/functions/generateSlug'; import generateSlug from '$lib/utils/functions/generateSlug';
import { t } from '$lib/utils/functions/translations';
let isSaving = false; let isSaving = false;
let isLoading = false; let isLoading = false;
@@ -87,9 +87,9 @@
a.click(); a.click();
a.remove(); a.remove();
snackbar.success('Download Complete'); snackbar.success('snackbar.course_settings.success.download');
} catch (error) { } catch (error) {
snackbar.error("Something's not right - Please try later"); snackbar.error('snackbar.course_settings.error.not_right');
} }
isLoading = false; isLoading = false;
}; };
@@ -105,18 +105,18 @@
isDeleting = false; isDeleting = false;
goto($currentOrgPath + '/courses'); goto($currentOrgPath + '/courses');
} catch (error) { } catch (error) {
snackbar.error('Something went wrong - Please try later'); snackbar.error('snackbar.course_settings.error.went_wrong');
isDeleting = false; isDeleting = false;
} }
} }
const handleSave = async () => { const handleSave = async () => {
if (!$settings.course_title) { if (!$settings.course_title) {
errors.title = 'title cannot be empty'; errors.title = $t('snackbar.course_settings.error.title');
return; return;
} }
if (!$settings.course_description) { if (!$settings.course_description) {
errors.description = 'description cannot be empty'; errors.description = $t('snackbar.course_settings.error.description');
return; return;
} }
isSaving = true; isSaving = true;
@@ -157,7 +157,7 @@
lessonDownload: lesson_download, lessonDownload: lesson_download,
allowNewStudent: allow_new_students allowNewStudent: allow_new_students
}; };
snackbar.success('Saved successfully'); snackbar.success('snackbar.course_settings.success.saved');
} catch (error) { } catch (error) {
snackbar.error(); snackbar.error();
} }
@@ -190,21 +190,20 @@
<Grid class="border-c rounded border-gray-200 dark:border-neutral-600"> <Grid class="border-c rounded border-gray-200 dark:border-neutral-600">
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<SectionTitle>Cover Image</SectionTitle> <SectionTitle>{$t('course.navItem.settings.cover_image')}</SectionTitle>
<p> <p>
This optional image will show up on your welcome page. If you include one, it should be at {$t('course.navItem.settings.optional_image')}
least 280 x 200"
</p> </p>
<span class="flex items-center justify-start"> <span class="flex items-center justify-start">
<PrimaryButton <PrimaryButton
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="Replace" label={$t('course.navItem.settings.replace')}
className="mr-2" className="mr-2"
onClick={widgetControl} onClick={widgetControl}
/> />
<PrimaryButton <PrimaryButton
variant={VARIANTS.CONTAINED_DANGER} variant={VARIANTS.CONTAINED_DANGER}
label="Delete" label={$t('course.navItem.settings.del')}
onClick={deleteBannerImage} onClick={deleteBannerImage}
/> />
</span> </span>
@@ -227,27 +226,27 @@
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<SectionTitle>Course details</SectionTitle> <SectionTitle>{$t('course.navItem.settings.course_details')}</SectionTitle>
</Column> </Column>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<TextField <TextField
label="Course title" label={$t('course.navItem.settings.course_title')}
placeholder="Write the course title here" placeholder="Write the course title here"
className="w-full mb-5" className="w-full mb-5"
bind:value={$settings.course_title} bind:value={$settings.course_title}
errorMessage={errors.title} errorMessage={errors.title}
/> />
<TextArea <TextArea
label="Course description" label={$t('course.navItem.settings.course_description')}
placeholder="Write your course description here" placeholder={$t('course.navItem.settings.placeholder')}
className="w-full mb-5" className="w-full mb-5"
bind:value={$settings.course_description} bind:value={$settings.course_description}
errorMessage={errors.description} errorMessage={errors.description}
/> />
<div class=""> <div class="">
<p class="text-md flex items-center gap-2 mb-2"> <p class="text-md flex items-center gap-2 mb-2">
Course Link {$t('course.navItem.settings.link')}
<IconButton contained={true} size="small" onClick={generateNewCourseLink}> <IconButton contained={true} size="small" onClick={generateNewCourseLink}>
<Restart size={16} /> <Restart size={16} />
</IconButton> </IconButton>
@@ -262,21 +261,21 @@
</Row> </Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<SectionTitle>Grading</SectionTitle> <SectionTitle>{$t('course.navItem.settings.grading')}</SectionTitle>
<p>Make grading reports available for lessons</p> <p>{$t('course.navItem.settings.reports')}</p>
</Column> </Column>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<Toggle size="sm" bind:toggled={$settings.grading}> <Toggle size="sm" bind:toggled={$settings.grading}>
<span slot="labelA" style="color: gray">Disabled</span> <span slot="labelA" style="color: gray">{$t('course.navItem.settings.disabled')}</span>
<span slot="labelB" style="color: gray">Enabled</span> <span slot="labelB" style="color: gray">{$t('course.navItem.settings.enabled')}</span>
</Toggle> </Toggle>
</Column> </Column>
</Row> </Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<SectionTitle>Order Lessons Tab</SectionTitle> <SectionTitle>{$t('course.navItem.settings.order')}</SectionTitle>
<p>Drag and drop the labels to re-order your material tabs</p> <p>{$t('course.navItem.settings.drag')}</p>
</Column> </Column>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<DragAndDrop /> <DragAndDrop />
@@ -284,24 +283,24 @@
</Row> </Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<SectionTitle>Lesson Download</SectionTitle> <SectionTitle>{$t('course.navItem.settings.lesson_download')}</SectionTitle>
<p>Make the Lesson available for Download in pdf for registered students</p> <p>{$t('course.navItem.settings.available')}</p>
</Column> </Column>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
{#if $isFreePlan} {#if $isFreePlan}
<UpgradeBanner>Upgrade to download lessons</UpgradeBanner> <UpgradeBanner>Upgrade to download lessons</UpgradeBanner>
{:else} {:else}
<Toggle size="sm" bind:toggled={$settings.lesson_download}> <Toggle size="sm" bind:toggled={$settings.lesson_download}>
<span slot="labelA" style="color: gray">Disabled</span> <span slot="labelA" style="color: gray">{$t('course.navItem.settings.disabled')}</span>
<span slot="labelB" style="color: gray">Enabled</span> <span slot="labelB" style="color: gray">{$t('course.navItem.settings.enabled')}</span>
</Toggle> </Toggle>
{/if} {/if}
</Column> </Column>
</Row> </Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<SectionTitle>Course Download</SectionTitle> <SectionTitle>{$t('course.navItem.settings.course_download')}</SectionTitle>
<p>Make the Course available for Download for registered students</p> <p>{$t('course.navItem.settings.course_avail')}</p>
</Column> </Column>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
{#if $isFreePlan} {#if $isFreePlan}
@@ -309,7 +308,7 @@
{:else} {:else}
<PrimaryButton <PrimaryButton
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="Download" label={$t('course.navItem.settings.download')}
onClick={downloadCourse} onClick={downloadCourse}
isDisabled={isLoading || !PUBLIC_SERVER_URL} isDisabled={isLoading || !PUBLIC_SERVER_URL}
{isLoading} {isLoading}
@@ -320,13 +319,13 @@
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<SectionTitle>Allow New Students</SectionTitle> <SectionTitle>{$t('course.navItem.settings.allow')}</SectionTitle>
<p>Allow new students to access this course</p> <p>{$t('course.navItem.settings.access')}</p>
</Column> </Column>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<Toggle size="sm" bind:toggled={$settings.allow_new_students}> <Toggle size="sm" bind:toggled={$settings.allow_new_students}>
<span slot="labelA" style="color: gray">Disabled</span> <span slot="labelA" style="color: gray">{$t('course.navItem.settings.disabled')}</span>
<span slot="labelB" style="color: gray">Enabled</span> <span slot="labelB" style="color: gray">{$t('course.navItem.settings.enabled')}</span>
</Toggle> </Toggle>
</Column> </Column>
</Row> </Row>
@@ -334,8 +333,8 @@
<!-- Publish Course --> <!-- Publish Course -->
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<SectionTitle>Publish Course</SectionTitle> <SectionTitle>{$t('course.navItem.settings.publish')}</SectionTitle>
<p>This determines if your course displays on your landing page</p> <p>{$t('course.navItem.settings.determines')}</p>
</Column> </Column>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<Toggle <Toggle
@@ -349,21 +348,21 @@
} }
}} }}
> >
<span slot="labelA" style="color: gray">Unpublished</span> <span slot="labelA" style="color: gray">{$t('course.navItem.settings.unpublished')}</span>
<span slot="labelB" style="color: gray">Published</span> <span slot="labelB" style="color: gray">{$t('course.navItem.settings.published')}</span>
</Toggle> </Toggle>
</Column> </Column>
</Row> </Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<SectionTitle>Delete course</SectionTitle> <SectionTitle>{$t('course.navItem.settings.delete')}</SectionTitle>
<p>Delete this course,this action cannot be undone</p> <p>{$t('course.navItem.settings.delete_text')}</p>
</Column> </Column>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<PrimaryButton <PrimaryButton
variant={VARIANTS.CONTAINED_DANGER} variant={VARIANTS.CONTAINED_DANGER}
label="Delete Course" label={$t('course.navItem.settings.delete')}
onClick={handleDeleteCourse} onClick={handleDeleteCourse}
isLoading={isDeleting} isLoading={isDeleting}
isDisabled={isDeleting} isDisabled={isDeleting}
@@ -372,7 +371,7 @@
</Row> </Row>
<Row class="p-5 w-full flex items-center justify-end"> <Row class="p-5 w-full flex items-center justify-end">
<PrimaryButton <PrimaryButton
label="Save Changes" label={$t('course.navItem.settings.save')}
isLoading={isSaving} isLoading={isSaving}
isDisabled={isSaving} isDisabled={isSaving}
onClick={handleSave} onClick={handleSave}
@@ -7,9 +7,9 @@ export const settings = writable({
grading: false, grading: false,
allow_new_students: false, allow_new_students: false,
tabs: [ tabs: [
{ id: 1, name: 'Note' }, { id: 1, name: 'course.navItem.lessons.materials.tabs.note.title' },
{ id: 2, name: 'Slide' }, { id: 2, name: 'course.navItem.lessons.materials.tabs.slide.title' },
{ id: 3, name: 'Video' } { id: 3, name: 'course.navItem.lessons.materials.tabs.video.title' }
], ],
lesson_download: false, lesson_download: false,
is_published: false is_published: false
@@ -38,9 +38,9 @@ export const defaultCourse: Course = {
imgUrl: '' imgUrl: ''
}, },
lessonTabsOrder: [ lessonTabsOrder: [
{ id: 1, name: 'Note' }, { id: 1, name: 'course.navItem.lessons.materials.tabs.note.title' },
{ id: 2, name: 'Slide' }, { id: 2, name: 'course.navItem.lessons.materials.tabs.slide.title' },
{ id: 3, name: 'Video' } { id: 3, name: 'course.navItem.lessons.materials.tabs.video.title' }
], ],
grading: false, grading: false,
lessonDownload: true, lessonDownload: true,
@@ -4,6 +4,7 @@
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { handleOpenWidget } from '../../store'; import { handleOpenWidget } from '../../store';
import { t } from '$lib/utils/functions/translations';
export let course = {}; export let course = {};
@@ -12,10 +13,15 @@
} }
</script> </script>
<TextField className="mt-5" labelClassName="font-bold" label="Title" bind:value={course.title} /> <TextField
className="mt-5"
labelClassName="font-bold"
label={$t('course.navItem.landing_page.editor.header_form.title')}
bind:value={course.title}
/>
<TextArea <TextArea
label="Description" label={$t('course.navItem.landing_page.editor.header_form.description')}
bind:value={course.description} bind:value={course.description}
rows="6" rows="6"
className="mt-5" className="mt-5"
@@ -25,13 +31,17 @@
<TextField <TextField
className="mt-5" className="mt-5"
labelClassName="font-bold" labelClassName="font-bold"
label="Short Video" label={$t('course.navItem.landing_page.editor.header_form.short_video')}
placeholder="www.youtube.com/watch?v=uYRq60G5XTk" placeholder="www.youtube.com/watch?v=uYRq60G5XTk"
helperMessage="Enter a link to a youtube video" helperMessage="Enter a link to a youtube video"
type="text" type="text"
bind:value={course.metadata.videoUrl} bind:value={course.metadata.videoUrl}
/> />
<div class="mt-7"> <div class="mt-7">
<p class="font-bold mb-3">Replace cover image</p> <p class="font-bold mb-3">{$t('course.navItem.landing_page.editor.header_form.replace_cover')}</p>
<PrimaryButton label="Replace" variant={VARIANTS.OUTLINED} onClick={widgetControl} /> <PrimaryButton
label={$t('course.navItem.landing_page.editor.header_form.replace')}
variant={VARIANTS.OUTLINED}
onClick={widgetControl}
/>
</div> </div>
@@ -8,6 +8,7 @@
import UploadImage from '$lib/components/UploadImage/index.svelte'; import UploadImage from '$lib/components/UploadImage/index.svelte';
import { uploadAvatar } from '$lib/utils/services/courses'; import { uploadAvatar } from '$lib/utils/services/courses';
import type { Course } from '$lib/utils/types'; import type { Course } from '$lib/utils/types';
import { t } from '$lib/utils/functions/translations';
export let course: Course; export let course: Course;
let name: string | undefined; let name: string | undefined;
@@ -60,29 +61,31 @@
</script> </script>
<div class="mt-5"> <div class="mt-5">
<label for="upload" class="font-bold">Instructor Logo</label> <label for="upload" class="font-bold"
>{$t('course.navItem.landing_page.editor.instructure_form.upload')}</label
>
<UploadImage bind:avatar src={imgUrl} /> <UploadImage bind:avatar src={imgUrl} />
</div> </div>
<TextField <TextField
className="mt-5" className="mt-5"
labelClassName="font-bold" labelClassName="font-bold"
label="Instructor name" label={$t('course.navItem.landing_page.editor.instructure_form.name')}
placeholder="Your company name" placeholder={$t('course.navItem.landing_page.editor.instructure_form.name_placeholder')}
bind:value={name} bind:value={name}
/> />
<TextField <TextField
className="mt-5" className="mt-5"
labelClassName="font-bold" labelClassName="font-bold"
label="Instructor Role" label={$t('course.navItem.landing_page.editor.instructure_form.role')}
placeholder="e.g Software developer" placeholder="e.g Software developer"
bind:value={role} bind:value={role}
/> />
<TextArea <TextArea
label="About instructor" label={$t('course.navItem.landing_page.editor.instructure_form.about')}
placeholder="A short background about the instructor" placeholder={$t('course.navItem.landing_page.editor.instructure_form.about_placeholder')}
rows={6} rows={6}
className="mt-5" className="mt-5"
labelClassName="font-bold" labelClassName="font-bold"
@@ -92,7 +95,7 @@
<TextField <TextField
className="mt-5" className="mt-5"
labelClassName="font-bold" labelClassName="font-bold"
label="Total number of courses" label={$t('course.navItem.landing_page.editor.instructure_form.total')}
type="number" type="number"
bind:value={courseNo} bind:value={courseNo}
/> />
@@ -9,6 +9,7 @@
import TextEditor from '$lib/components/TextEditor/index.svelte'; import TextEditor from '$lib/components/TextEditor/index.svelte';
import { isCourseFree } from '$lib/utils/functions/course'; import { isCourseFree } from '$lib/utils/functions/course';
import type { Course } from '$lib/utils/types'; import type { Course } from '$lib/utils/types';
import { t } from '$lib/utils/functions/translations';
export let course: Course; export let course: Course;
@@ -53,7 +54,10 @@
</script> </script>
{#if typeof course !== 'undefined'} {#if typeof course !== 'undefined'}
<Select labelText="Currency" bind:selected={course.currency}> <Select
labelText={$t('course.navItem.landing_page.editor.pricing_form.currency')}
bind:selected={course.currency}
>
<SelectItem value="NGN" /> <SelectItem value="NGN" />
<SelectItem value="USD" /> <SelectItem value="USD" />
</Select> </Select>
@@ -61,7 +65,7 @@
<TextField <TextField
className="mt-5" className="mt-5"
labelClassName="font-bold" labelClassName="font-bold"
label="Cost" label={$t('course.navItem.landing_page.editor.pricing_form.cost')}
type="number" type="number"
bind:value={course.cost} bind:value={course.cost}
/> />
@@ -70,7 +74,7 @@
<TextField <TextField
className="mt-5" className="mt-5"
labelClassName="font-bold" labelClassName="font-bold"
label="Payment Link" label={$t('course.navItem.landing_page.editor.pricing_form.payment')}
helperMessage="Stripe, Lemon Squeezy or any payment link" helperMessage="Stripe, Lemon Squeezy or any payment link"
bind:value={paymentLink} bind:value={paymentLink}
/> />
@@ -78,9 +82,12 @@
<div class="mt-5"> <div class="mt-5">
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> --> <!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> -->
<Toggle labelText="Discount" bind:toggled={showDiscount}> <Toggle
<span slot="labelA">No</span> labelText={$t('course.navItem.landing_page.editor.pricing_form.discount')}
<span slot="labelB">Yes</span> bind:toggled={showDiscount}
>
<span slot="labelA">{$t('course.navItem.landing_page.editor.pricing_form.no')}</span>
<span slot="labelB">{$t('course.navItem.landing_page.editor.pricing_form.yes')}</span>
</Toggle> </Toggle>
</div> </div>
@@ -88,7 +95,7 @@
<TextField <TextField
className="mt-5" className="mt-5"
labelClassName="font-bold" labelClassName="font-bold"
label="Discout Percent" label={$t('course.navItem.landing_page.editor.pricing_form.percent')}
type="number" type="number"
bind:value={discount} bind:value={discount}
helperMessage="In Percentage %" helperMessage="In Percentage %"
@@ -98,13 +105,15 @@
<div class="mt-5"> <div class="mt-5">
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> --> <!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> -->
<Toggle labelText="Gift on Completion" bind:toggled={giftToggled}> <Toggle labelText="Gift on Completion" bind:toggled={giftToggled}>
<span slot="labelA">No</span> <span slot="labelA">{$t('course.navItem.landing_page.editor.pricing_form.no')}</span>
<span slot="labelB">Yes</span> <span slot="labelB">{$t('course.navItem.landing_page.editor.pricing_form.yes')}</span>
</Toggle> </Toggle>
</div> </div>
{#if giftToggled} {#if giftToggled}
<p class="dark:text-white font-bold mt-5">About Gift</p> <p class="dark:text-white font-bold mt-5">
{$t('course.navItem.landing_page.editor.pricing_form.gift')}
</p>
<div class="h-2/5"> <div class="h-2/5">
<TextEditor value={get(course, 'metadata.reward.description', '')} onChange={handleChange} /> <TextEditor value={get(course, 'metadata.reward.description', '')} onChange={handleChange} />
@@ -7,6 +7,7 @@
import TextArea from '$lib/components/Form/TextArea.svelte'; import TextArea from '$lib/components/Form/TextArea.svelte';
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import { uploadAvatar } from '$lib/utils/services/courses'; import { uploadAvatar } from '$lib/utils/services/courses';
import { t } from '$lib/utils/functions/translations';
export let courseId; export let courseId;
export let reviews = []; export let reviews = [];
@@ -42,7 +43,7 @@
<TextField <TextField
className="mt-2 w-full" className="mt-2 w-full"
labelClassName="font-normal" labelClassName="font-normal"
label="Full Name" label={$t('course.navItem.landing_page.editor.reviews_form.fullname')}
placeholder="" placeholder=""
type="text" type="text"
bind:value={review.name} bind:value={review.name}
@@ -51,7 +52,7 @@
<!-- Description --> <!-- Description -->
<TextArea <TextArea
label="Description" label={$t('course.navItem.landing_page.editor.reviews_form.description')}
rows="4" rows="4"
className="mt-2 w-full" className="mt-2 w-full"
labelClassName="font-normal" labelClassName="font-normal"
@@ -68,7 +69,7 @@
<TextField <TextField
className="mt-2 w-20" className="mt-2 w-20"
labelClassName="font-normal" labelClassName="font-normal"
label="Rating" label={$t('course.navItem.landing_page.editor.reviews_form.rating')}
type="number" type="number"
min={1} min={1}
max={5} max={5}
@@ -78,7 +79,11 @@
<!-- Toggle --> <!-- Toggle -->
<div class="w-24"> <div class="w-24">
<Toggle labelText="Hide Review" size="sm" bind:toggled={review.hide} /> <Toggle
labelText={$t('course.navItem.landing_page.editor.reviews_form.hide')}
size="sm"
bind:toggled={review.hide}
/>
</div> </div>
</div> </div>
@@ -25,6 +25,7 @@
import { isMobile } from '$lib/utils/store/useMobile'; import { isMobile } from '$lib/utils/store/useMobile';
import CustomPromptBtn from '$lib/components/AI/AIButton/CustomPromptBtn.svelte'; import CustomPromptBtn from '$lib/components/AI/AIButton/CustomPromptBtn.svelte';
import type { Course } from '$lib/utils/types'; import type { Course } from '$lib/utils/types';
import { t } from '$lib/utils/functions/translations';
export let course: Course; export let course: Course;
export let courseId: string; export let courseId: string;
@@ -46,43 +47,43 @@
{ {
key: 1, key: 1,
path: '', path: '',
title: 'Header' title: $t('course.navItem.landing_page.editor.title.1')
}, },
{ {
key: 2, key: 2,
path: 'metadata.requirements', path: 'metadata.requirements',
title: 'Requirement', title: $t('course.navItem.landing_page.editor.title.2'),
enableAIWriter: true, enableAIWriter: true,
initPrompt: 'Please write few requirements needed to take this course:' initPrompt: 'Please write few requirements needed to take this course:'
}, },
{ {
key: 3, key: 3,
path: 'metadata.description', path: 'metadata.description',
title: 'Description', title: $t('course.navItem.landing_page.editor.title.3'),
enableAIWriter: true, enableAIWriter: true,
initPrompt: 'Please write a detailed course description for this course:' initPrompt: 'Please write a detailed course description for this course:'
}, },
{ {
key: 4, key: 4,
path: 'metadata.goals', path: 'metadata.goals',
title: 'Goals', title: $t('course.navItem.landing_page.editor.title.4'),
enableAIWriter: true, enableAIWriter: true,
initPrompt: 'What should a student expect to learn from this course:' initPrompt: 'What should a student expect to learn from this course:'
}, },
{ {
key: 5, key: 5,
path: '', path: '',
title: 'Reviews' title: $t('course.navItem.landing_page.editor.title.5')
}, },
{ {
key: 6, key: 6,
path: '', path: '',
title: 'Instructor' title: $t('course.navItem.landing_page.editor.title.6')
}, },
{ {
key: 7, key: 7,
path: '', path: '',
title: 'Pricing' title: $t('course.navItem.landing_page.editor.title.7')
} }
]; ];
let selectedSection: Section | null = null; let selectedSection: Section | null = null;
@@ -168,7 +169,7 @@
<CloseButton onClick={handleClose} /> <CloseButton onClick={handleClose} />
<div class="flex items-center"> <div class="flex items-center">
<PrimaryButton <PrimaryButton
label="Save" label={$t('course.navItem.landing_page.editor.save')}
type="button" type="button"
className="mr-1" className="mr-1"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
@@ -181,7 +182,7 @@
</div> </div>
</div> </div>
<div class="flex justify-between items-center px-2 w-full mb-2"> <div class="flex justify-between items-center px-2 w-full mb-2">
<h3 class="dark:text-white">Page Builder</h3> <h3 class="dark:text-white">{$t('course.navItem.landing_page.editor.page_builder')}</h3>
</div> </div>
{#each sections as section, index} {#each sections as section, index}
<button <button
@@ -189,7 +190,10 @@
sections.length && 'border-b-0'} border-gray-300" sections.length && 'border-b-0'} border-gray-300"
on:click={handleSectionSelect(section.key)} on:click={handleSectionSelect(section.key)}
> >
<p class="dark:text-white mr-2">{section.title} section</p> <p class="dark:text-white mr-2">
{section.title}
{$t('course.navItem.landing_page.editor.section')}
</p>
<ChevronRightIcon size={24} class="carbon-class" /> <ChevronRightIcon size={24} class="carbon-class" />
</button> </button>
{/each} {/each}
@@ -10,6 +10,7 @@
import type { Course } from '$lib/utils/types'; import type { Course } from '$lib/utils/types';
import { ROLE } from '$lib/utils/constants/roles'; import { ROLE } from '$lib/utils/constants/roles';
import { capturePosthogEvent } from '$lib/utils/services/posthog'; import { capturePosthogEvent } from '$lib/utils/services/posthog';
import { t } from '$lib/utils/functions/translations';
export let className = ''; export let className = '';
export let editMode = false; export let editMode = false;
@@ -92,12 +93,14 @@
<p class="dark:text-white font-medium text-sm flex items-center gap-1"> <p class="dark:text-white font-medium text-sm flex items-center gap-1">
{formatter?.format(calculatedCost) || calculatedCost} {formatter?.format(calculatedCost) || calculatedCost}
{#if isFree} {#if isFree}
<span class="text-xs">(Free)</span> <span class="text-xs"
>({$t('course.navItem.landing_page.pricing_section.free')})</span
>
{/if} {/if}
</p> </p>
{#if courseData?.metadata?.showDiscount} {#if courseData?.metadata?.showDiscount}
<p class="dark:text-white font-light text-sm text-gray-500"> <p class="dark:text-white font-light text-sm text-gray-500">
{discount}% Discount. {discount}% {$t('course.navItem.landing_page.pricing_section.discount')}.
<span class="line-through" <span class="line-through"
>{formatter?.format(courseData?.cost || 0) || courseData.cost}</span >{formatter?.format(courseData?.cost || 0) || courseData.cost}</span
> >
@@ -105,7 +108,7 @@
{/if} {/if}
{:else} {:else}
<p class="dark:text-white text-lg"> <p class="dark:text-white text-lg">
This course is currently not accepting new students. {$t('course.navItem.landing_page.pricing_section.not_accepting')}
</p> </p>
{/if} {/if}
</div> </div>
@@ -113,7 +116,9 @@
<!-- Call To Action Buttons --> <!-- Call To Action Buttons -->
<div class="flex flex-col w-full h-full items-center"> <div class="flex flex-col w-full h-full items-center">
<PrimaryButton <PrimaryButton
label={isFree ? 'Enroll Now' : 'Buy Now'} label={isFree
? $t('course.navItem.landing_page.pricing_section.enroll')
: $t('course.navItem.landing_page.pricing_section.buy')}
className="w-full sm:w-full h-[40px]" className="w-full sm:w-full h-[40px]"
onClick={handleJoinCourse} onClick={handleJoinCourse}
isDisabled={!courseData.metadata.allowNewStudent} isDisabled={!courseData.metadata.allowNewStudent}
@@ -135,12 +140,13 @@
<p class="dark:text-white font-medium text-lg"> <p class="dark:text-white font-medium text-lg">
{formatter?.format(calculatedCost) || calculatedCost} {formatter?.format(calculatedCost) || calculatedCost}
{#if isFree} {#if isFree}
<span class="text-sm">(Free)</span> <span class="text-sm">({$t('course.navItem.landing_page.pricing_section.free')})</span
>
{/if} {/if}
</p> </p>
{#if courseData?.metadata?.showDiscount} {#if courseData?.metadata?.showDiscount}
<p class="dark:text-white font-light text-sm text-gray-500"> <p class="dark:text-white font-light text-sm text-gray-500">
{discount}% Discount. {discount}% {$t('course.navItem.landing_page.pricing_section.discount')}.
<span class="line-through" <span class="line-through"
>{formatter?.format(courseData?.cost || 0) || courseData.cost}</span >{formatter?.format(courseData?.cost || 0) || courseData.cost}</span
> >
@@ -148,7 +154,7 @@
{/if} {/if}
{:else} {:else}
<p class="dark:text-white text-lg"> <p class="dark:text-white text-lg">
This course is currently not accepting new students. {$t('course.navItem.landing_page.pricing_section.not_accepting')}
</p> </p>
{/if} {/if}
</div> </div>
@@ -156,13 +162,15 @@
<!-- Call To Action Buttons --> <!-- Call To Action Buttons -->
<div class="flex flex-col w-full items-center"> <div class="flex flex-col w-full items-center">
<PrimaryButton <PrimaryButton
label={isFree ? 'Enroll Now' : 'Buy Now'} label={isFree
? $t('course.navItem.landing_page.pricing_section.enroll')
: $t('course.navItem.landing_page.pricing_section.buy')}
className="w-full sm:w-full py-3 mb-3" className="w-full sm:w-full py-3 mb-3"
onClick={handleJoinCourse} onClick={handleJoinCourse}
isDisabled={!courseData.metadata.allowNewStudent} isDisabled={!courseData.metadata.allowNewStudent}
/> />
{#if courseData?.metadata?.showDiscount && courseData.metadata.allowNewStudent} {#if courseData?.metadata?.showDiscount && courseData.metadata.allowNewStudent}
<p class="dark:text-white font-light text-sm text-gray-500">Early bird offer. Buy ASAP</p> <p class="dark:text-white font-light text-sm text-gray-500">{$t('course.navItem.landing_page.pricing_section.bird')}</p>
{/if} {/if}
</div> </div>
</div> </div>
@@ -1,3 +1,5 @@
// import { t } from '$lib/utils/functions/translations';
export const SECTION_KEYS = { export const SECTION_KEYS = {
HEADER: 'header', HEADER: 'header',
REQUIREMENT: '#requirement', REQUIREMENT: '#requirement',
@@ -11,26 +13,26 @@ export const SECTION_KEYS = {
export const NAV_ITEMS = [ export const NAV_ITEMS = [
{ {
key: SECTION_KEYS.REQUIREMENT, key: SECTION_KEYS.REQUIREMENT,
label: 'Requirement' label: 'course.navItem.landing_page.editor.title.2'
}, },
{ {
key: SECTION_KEYS.DESCRIPTION, key: SECTION_KEYS.DESCRIPTION,
label: 'Description' label: 'course.navItem.landing_page.editor.title.3'
}, },
{ {
key: SECTION_KEYS.GOALS, key: SECTION_KEYS.GOALS,
label: 'Goals' label: 'course.navItem.landing_page.editor.title.4'
}, },
{ {
key: SECTION_KEYS.LESSONS, key: SECTION_KEYS.LESSONS,
label: 'Lessons' label: 'course.navItems.2'
}, },
{ {
key: SECTION_KEYS.REVIEWS, key: SECTION_KEYS.REVIEWS,
label: 'Reviews' label: 'course.navItem.landing_page.editor.title.5'
}, },
{ {
key: SECTION_KEYS.INSTRUCTOR, key: SECTION_KEYS.INSTRUCTOR,
label: 'Instructor' label: 'course.navItem.landing_page.editor.title.6'
} }
]; ];
@@ -26,6 +26,7 @@
import { currentOrg } from '$lib/utils/store/org'; import { currentOrg } from '$lib/utils/store/org';
import UploadWidget from '$lib/components/UploadWidget/index.svelte'; import UploadWidget from '$lib/components/UploadWidget/index.svelte';
import { course } from '$lib/components/Course/store'; import { course } from '$lib/components/Course/store';
import { t } from '$lib/utils/functions/translations';
export let editMode = false; export let editMode = false;
export let courseData: Course; export let courseData: Course;
@@ -124,7 +125,7 @@
{get(courseData, 'metadata.instructor.name', '')} {get(courseData, 'metadata.instructor.name', '')}
</p> </p>
<PrimaryButton <PrimaryButton
label="Start Course" label={$t('course.navItem.landing_page.start_course')}
className="px-6 py-5 mt-6 sm:w-fit hidden md:block" className="px-6 py-5 mt-6 sm:w-fit hidden md:block"
onClick={() => { onClick={() => {
if (editMode) return; if (editMode) return;
@@ -207,14 +208,17 @@
class="{navItem.key === activeNav && class="{navItem.key === activeNav &&
'active text-primary-700'} z-0 mr-6 rounded-lg px-2 font-normal text-slate-700 hover:bg-gray-200 dark:text-white dark:hover:text-slate-900" 'active text-primary-700'} z-0 mr-6 rounded-lg px-2 font-normal text-slate-700 hover:bg-gray-200 dark:text-white dark:hover:text-slate-900"
> >
{navItem.label} {$t(navItem.label)}
</a> </a>
{/each} {/each}
</nav> </nav>
<!-- Sections - Requirement --> <!-- Sections - Requirement -->
<section id="requirement" class="mt-8 border-b border-gray-300 pb-10"> <section id="requirement" class="mt-8 border-b border-gray-300 pb-10">
<h3 class="mb-3 mt-0 text-2xl font-bold">Requirement</h3> <h3 class="mb-3 mt-0 text-2xl font-bold">
{$t('course.navItem.landing_page.requirement')}
</h3>
<ul class="list font-light"> <ul class="list font-light">
<HtmlRender content={get(courseData, 'metadata.requirements', '')} /> <HtmlRender content={get(courseData, 'metadata.requirements', '')} />
</ul> </ul>
@@ -222,7 +226,10 @@
<!-- Sections - Course Description --> <!-- Sections - Course Description -->
<section id="description" class="mt-8 border-b border-gray-300 pb-10"> <section id="description" class="mt-8 border-b border-gray-300 pb-10">
<h3 class="mb-3 mt-0 text-2xl font-bold">Course Description</h3> <h3 class="mb-3 mt-0 text-2xl font-bold">
{$t('course.navItem.landing_page.description')}
</h3>
<HtmlRender <HtmlRender
className="dark:text-white text-sm font-light" className="dark:text-white text-sm font-light"
content={get(courseData, 'metadata.description', '')} content={get(courseData, 'metadata.description', '')}
@@ -231,27 +238,28 @@
<!-- Sections - Goal --> <!-- Sections - Goal -->
<section id="goals" class="mt-8 pb-10"> <section id="goals" class="mt-8 pb-10">
<h3 class="mb-3 mt-0 text-2xl font-bold">What you'll learn</h3> <h3 class="text-2xl font-bold mt-0 mb-3">{$t('course.navItem.landing_page.learn')}</h3>
<ul class="list font-light"> <ul class="list font-light">
<HtmlRender content={get(courseData, 'metadata.goals', '')} /> <HtmlRender content={get(courseData, 'metadata.goals', '')} />
</ul> </ul>
</section> </section>
<!-- Sections - Certificate --> <!-- Sections - Certificate -->
<section class="mt-8 border-b border-gray-300 pb-10"> <section class="border-b border-gray-300 mt-8 pb-10">
<h3 class="mt-0 text-2xl font-bold">Certificate</h3> <h3 class="text-2xl font-bold mt-0">{$t('course.navItem.landing_page.certificate')}</h3>
<p class="mb-3 text-sm font-light dark:text-white"> <p class="dark:text-white text-sm font-light mb-3">
When you complete all of the courses in the program, you'll earn a Certificate to share {$t('course.navItem.landing_page.certificate_text')}
with your professional network.
</p> </p>
<img src="/images/certificate-template.svg" alt="certificate template" /> <img src="/images/certificate-template.svg" alt="certificate template" />
</section> </section>
<!-- Sections - Lessons --> <!-- Sections - Lessons -->
<section id="lessons" class="mt-8 border-b border-gray-300 pb-10"> <section id="lessons" class="border-b border-gray-300 mt-8 pb-10">
<div class="mb-3 flex w-full items-center justify-between"> <div class="flex items-center justify-between w-full mb-3">
<h3 class="mb-3 mt-0 text-2xl font-bold">Course Content</h3> <h3 class="text-2xl font-bold mt-0 mb-3">
<p class="text-sm font-light dark:text-white"> {$t('course.navItem.landing_page.content')}
</h3>
<p class="dark:text-white text-sm font-light">
<!-- {lessons.length} lessons --> <!-- {lessons.length} lessons -->
{pluralize('lesson', lessons.length, true)} {pluralize('lesson', lessons.length, true)}
</p> </p>
@@ -271,23 +279,25 @@
<div class="w-2/4"> <div class="w-2/4">
<div class="flex items-center"> <div class="flex items-center">
{#if lesson.slide_url} {#if lesson.slide_url}
<span class="flex w-2/4 text-sm font-light" <span class="text-sm font-light flex w-2/4"
><PresentationFile size={16} class="mr-1" />1 slide</span ><PresentationFile size={16} class="mr-1" />{$t(
'course.navItem.landing_page.slide'
)}</span
> >
{/if} {/if}
{#if lesson.note} {#if lesson.note}
<span class="flex w-2/4 text-sm font-light" <span class="text-sm font-light flex w-2/4"
><Notebook size={16} class="mr-1" />note</span ><Notebook size={16} class="mr-1" />{$t(
'course.navItem.landing_page.note'
)}</span
> >
{/if} {/if}
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
{#if lesson.videos} {#if lesson.videos}
<span class="flex w-2/4 text-sm font-light" <span class="text-sm font-light flex w-2/4"
><Video size={16} class="mr-1" />{lesson.videos.length} video{lesson.videos ><Video size={16} class="mr-1" />{lesson.videos.length}
.length > 1 {$t('course.navItem.landing_page.video')}{lesson.videos.length > 1 ? 's' : ''}
? 's'
: ''}
</span> </span>
{/if} {/if}
{#if get(lesson, 'totalExercises[0].count')} {#if get(lesson, 'totalExercises[0].count')}
@@ -308,7 +318,9 @@
<!-- Sections - Reviews --> <!-- Sections - Reviews -->
{#if reviews && reviews.length > 0} {#if reviews && reviews.length > 0}
<section id="reviews"> <section id="reviews">
<h2 class="my-16 mb-6 ml-0 mr-0 font-semibold">Reviews</h2> <h2 class="my-16 mr-0 mb-6 ml-0 font-semibold">
{$t('course.navItem.landing_page.reviews')}
</h2>
<div class="flex flex-wrap"> <div class="flex flex-wrap">
{#each reviews.slice(0, 4) as review, id} {#each reviews.slice(0, 4) as review, id}
{#if !review.hide} {#if !review.hide}
@@ -355,7 +367,7 @@
</div> </div>
{#if reviews.length > 4} {#if reviews.length > 4}
<PrimaryButton <PrimaryButton
label="See All" label={$t('course.navItem.landing_page.see_all')}
className="w-3/12 p-4 mt-2" className="w-3/12 p-4 mt-2"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
onClick={() => ($reviewsModalStore.open = true)} onClick={() => ($reviewsModalStore.open = true)}
@@ -367,16 +379,18 @@
onClose={() => ($reviewsModalStore.open = false)} onClose={() => ($reviewsModalStore.open = false)}
bind:open={$reviewsModalStore.open} bind:open={$reviewsModalStore.open}
width="w-9/12" width="w-9/12"
modalHeading="Reviews" modalHeading={$t('course.navItem.landing_page.reviews_modal.title')}
> >
<div class="flex"> <div class="flex">
<!-- ratings --> <!-- ratings -->
<div class="w-1/3"> <div class="w-1/3">
<h2 class="text-xl font-bold"> <h2 class="text-xl font-bold">
{averageRating} Rating {averageRating}
{$t('course.navItem.landing_page.reviews_modal.rating')}
</h2> </h2>
<h2 class="mt-2 text-lg font-semibold"> <h2 class="text-lg font-semibold mt-2">
{reviews.length} Ratings {reviews.length}
{$t('course.navItem.landing_page.reviews_modal.ratings')}
</h2> </h2>
</div> </div>
<!-- reviews --> <!-- reviews -->
@@ -418,8 +432,10 @@
<!-- Sections - Instructor --> <!-- Sections - Instructor -->
<section id="instructor" class="mt-8 pb-10"> <section id="instructor" class="mt-8 pb-10">
<h3 class="mb-3 mt-0 text-2xl font-bold">Instructor</h3> <h3 class="text-2xl font-bold mt-0 mb-3">
<div class="mb-4 flex items-center"> {$t('course.navItem.landing_page.instructor')}
</h3>
<div class="flex items-center mb-4">
<img <img
alt="Author Avatar" alt="Author Avatar"
class="mr-3 block h-20 w-20 rounded-full" class="mr-3 block h-20 w-20 rounded-full"
@@ -434,7 +450,10 @@
</p> </p>
<p class="text-md flex items-center font-light dark:text-white"> <p class="text-md flex items-center font-light dark:text-white">
<PlayFilled size={16} class="text-primary-700" /> <PlayFilled size={16} class="text-primary-700" />
<span class="ml-1">{get(instructor, 'courseNo', 10)} courses</span> <span class="ml-1"
>{get(instructor, 'courseNo', '')}
{$t('course.navItem.landing_page.courses')}</span
>
</p> </p>
</div> </div>
</div> </div>
@@ -13,6 +13,7 @@
import CopyFile from 'carbon-icons-svelte/lib/CopyFile.svelte'; import CopyFile from 'carbon-icons-svelte/lib/CopyFile.svelte';
import Share from 'carbon-icons-svelte/lib/Share.svelte'; import Share from 'carbon-icons-svelte/lib/Share.svelte';
import UserFollow from 'carbon-icons-svelte/lib/UserFollow.svelte'; import UserFollow from 'carbon-icons-svelte/lib/UserFollow.svelte';
import { t } from '$lib/utils/functions/translations';
export let bannerImage: string | undefined; export let bannerImage: string | undefined;
export let id = ''; export let id = '';
@@ -55,11 +56,30 @@
{#if showContextMenu} {#if showContextMenu}
<ContextMenu {target}> <ContextMenu {target}>
<ContextMenuOption indented labelText="Clone" icon={CopyFile} on:click={handleCloneCourse} /> <ContextMenuOption
<ContextMenuOption indented labelText="Share" icon={Share} on:click={handleShareCourse} /> indented
<ContextMenuOption indented labelText="Invite" icon={UserFollow} on:click={handleInvite} /> labelText={$t('courses.lesson_card.context_menu.clone')}
icon={CopyFile}
on:click={handleCloneCourse}
/>
<ContextMenuOption
indented
labelText={$t('courses.lesson_card.context_menu.share')}
icon={Share}
on:click={handleShareCourse}
/>
<ContextMenuOption
indented
labelText={$t('courses.lesson_card.context_menu.invite')}
icon={UserFollow}
on:click={handleInvite}
/>
<ContextMenuDivider /> <ContextMenuDivider />
<ContextMenuOption kind="danger" labelText="Delete" on:click={handleDeleteCourse} /> <ContextMenuOption
kind="danger"
labelText={$t('courses.lesson_card.context_menu.delete')}
on:click={handleDeleteCourse}
/>
</ContextMenu> </ContextMenu>
{/if} {/if}
@@ -79,7 +99,7 @@
<svelte:fragment slot="loading"> <svelte:fragment slot="loading">
<SkeletonPlaceholder style="width: 100%; height: 200px;" /> <SkeletonPlaceholder style="width: 100%; height: 200px;" />
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="error">An error occurred.</svelte:fragment> <svelte:fragment slot="error">{$t('courses.lesson_card.error_message')}</svelte:fragment>
</ImageLoader> </ImageLoader>
</div> </div>
@@ -94,7 +114,10 @@
'items-center'}" 'items-center'}"
> >
<div> <div>
<p class="text-xs pt-2 {!isLMS && 'pl-2'} dark:text-white">{totalLessons} lessons</p> <p class="text-xs pt-2 {!isLMS && 'pl-2'} dark:text-white">
{totalLessons}
{$t('courses.lesson_card.lessons_number')}
</p>
<p class="text-xs py-2"> <p class="text-xs py-2">
{#if isOnLandingPage} {#if isOnLandingPage}
<span class="px-2">{!cost ? 'Free' : formatter.format(cost)}</span> <span class="px-2">{!cost ? 'Free' : formatter.format(cost)}</span>
@@ -111,9 +134,9 @@
{:else} {:else}
<Tag type={isPublished ? 'green' : 'cool-gray'}> <Tag type={isPublished ? 'green' : 'cool-gray'}>
{#if isPublished} {#if isPublished}
Published {$t('courses.lesson_card.published')}
{:else} {:else}
Unpublished {$t('courses.lesson_card.unpublished')}
{/if} {/if}
</Tag> </Tag>
{/if} {/if}
@@ -122,13 +145,16 @@
{#if isLMS} {#if isLMS}
<PrimaryButton <PrimaryButton
label="Continue Course" label={$t('courses.lesson_card.continue_course')}
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
className="rounded-none text-primary-600" className="rounded-none text-primary-600"
/> />
{:else if !isOnLandingPage} {:else if !isOnLandingPage}
<div class="flex flex-col justify-between"> <div class="flex flex-col justify-between">
<p class="text-xs pt-2 pl-2 dark:text-white">{totalStudents} students</p> <p class="text-xs pt-2 pl-2 dark:text-white">
{totalStudents}
{$t('courses.lesson_card.students')}
</p>
<div></div> <div></div>
</div> </div>
{/if} {/if}
@@ -12,6 +12,7 @@
import { currentOrg } from '$lib/utils/store/org'; import { currentOrg } from '$lib/utils/store/org';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { capturePosthogEvent } from '$lib/utils/services/posthog'; import { capturePosthogEvent } from '$lib/utils/services/posthog';
import { t } from '$lib/utils/functions/translations';
let isLoading = false; let isLoading = false;
let errors = { let errors = {
@@ -118,13 +119,13 @@
{onClose} {onClose}
bind:open={$createCourseModal.open} bind:open={$createCourseModal.open}
width="w-4/5 md:w-2/5" width="w-4/5 md:w-2/5"
modalHeading="Create a Course" modalHeading={$t('courses.new_course_modal.heading')}
> >
<form on:submit|preventDefault={createCourse}> <form on:submit|preventDefault={createCourse}>
<TextField <TextField
label="Course name" label={$t('courses.new_course_modal.course_name')}
bind:value={$createCourseModal.title} bind:value={$createCourseModal.title}
placeholder="Your course name" placeholder={$t('courses.new_course_modal.course_name_placeholder')}
className="mb-4" className="mb-4"
isRequired={true} isRequired={true}
errorMessage={errors.title} errorMessage={errors.title}
@@ -132,10 +133,10 @@
/> />
<TextArea <TextArea
label="Short description" label={$t('courses.new_course_modal.short_description')}
bind:value={$createCourseModal.description} bind:value={$createCourseModal.description}
rows={4} rows={4}
placeholder="A little description about this course" placeholder={$t('courses.new_course_modal.short_description_placeholder')}
className="mb-4" className="mb-4"
isRequired={true} isRequired={true}
errorMessage={errors.description} errorMessage={errors.description}
@@ -146,7 +147,7 @@
<div class="mt-5 flex items-center"> <div class="mt-5 flex items-center">
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
label="Finish" label={$t('courses.new_course_modal.button')}
type="submit" type="submit"
isDisabled={isLoading} isDisabled={isLoading}
{isLoading} {isLoading}
@@ -6,11 +6,11 @@
import { courseMetaDeta } from './store'; import { courseMetaDeta } from './store';
import type { Course } from '$lib/utils/types'; import type { Course } from '$lib/utils/types';
import { globalStore } from '$lib/utils/store/app'; import { globalStore } from '$lib/utils/store/app';
import { t } from '$lib/utils/functions/translations';
export let courses: Course[] = []; export let courses: Course[] = [];
export let emptyTitle = 'No Courses Created'; export let emptyTitle = $t('courses.lesson_card.empty_title');
export let emptyDescription = export let emptyDescription = $t('courses.lesson_card.empty_description');
'Share your knowledge with the world by creating engaging courses for your students.';
function calcProgressRate(progressRate?: number, totalLessons?: number): number { function calcProgressRate(progressRate?: number, totalLessons?: number): number {
if (!progressRate || !totalLessons) { if (!progressRate || !totalLessons) {
@@ -1,10 +1,11 @@
<script lang="ts"> <script lang="ts">
import CustomPromptBtn from '$lib/components/AI/AIButton/CustomPromptBtn.svelte'; import CustomPromptBtn from '$lib/components/AI/AIButton/CustomPromptBtn.svelte';
import type { PopoverProps } from 'carbon-components-svelte/types/Popover/Popover.svelte'; import type { PopoverProps } from 'carbon-components-svelte/types/Popover/Popover.svelte';
import { t } from '$lib/utils/functions/translations';
export let label = ''; export let label = '';
export let disabled = false; export let disabled = false;
export let placeholder = 'Write your answer...'; export let placeholder = $t('course.navItem.lessons.exercises.all_exercises.write_your_answer');
export let value = ''; export let value = '';
export let rows = 3; export let rows = 3;
// export let maxRows = 3; // export let maxRows = 3;
@@ -10,6 +10,13 @@
import { profile } from '$lib/utils/store/user'; import { profile } from '$lib/utils/store/user';
import { NavClasses } from '$lib/utils/constants/reusableClass'; import { NavClasses } from '$lib/utils/constants/reusableClass';
import { sideBar } from '$lib/components/Org/store'; import { sideBar } from '$lib/components/Org/store';
import { t } from '$lib/utils/functions/translations';
interface sideLinks {
name: string;
icon: any;
link: string;
}
function isActive(pagePath: string, itemPath: string) { function isActive(pagePath: string, itemPath: string) {
const pageLinkItems = pagePath.split('/'); const pageLinkItems = pagePath.split('/');
@@ -21,24 +28,27 @@
return pagePath.includes(itemPath); return pagePath.includes(itemPath);
} }
let sideLinks = [
let sideLinks: sideLinks[] = [];
$: sideLinks = [
{ {
name: 'Home', name: $t('lms_navigation.home'),
icon: HomeIcon, icon: HomeIcon,
link: '/lms' link: '/lms'
}, },
{ {
name: 'My Learning', name: $t('lms_navigation.my_learning'),
icon: CourseIcon, icon: CourseIcon,
link: '/lms/mylearning' link: '/lms/mylearning'
}, },
{ {
name: 'Exercise', name: $t('lms_navigation.exercise'),
icon: LicenseDraft, icon: LicenseDraft,
link: '/lms/exercises' link: '/lms/exercises'
}, },
{ {
name: 'Community', name: $t('lms_navigation.community'),
icon: CommunityIcon, icon: CommunityIcon,
link: '/lms/community' link: '/lms/community'
} }
@@ -98,7 +108,7 @@
<a href="/lms" class="text-black" on:click={toggleSidebar}> <a href="/lms" class="text-black" on:click={toggleSidebar}>
<li class="flex items-center py-3 px-4 mb-2 rounded"> <li class="flex items-center py-3 px-4 mb-2 rounded">
<HelpIcon size={20} class="carbon-icon dark:text-white" /> <HelpIcon size={20} class="carbon-icon dark:text-white" />
<p class="dark:text-white ml-2">Help</p> <p class="dark:text-white ml-2">{$t('lms_navigation.help')}</p>
</li> </li>
</a> </a>
<a href="/lms/settings" class="text-black" on:click={toggleSidebar}> <a href="/lms/settings" class="text-black" on:click={toggleSidebar}>
@@ -109,7 +119,7 @@
) && NavClasses.active}" ) && NavClasses.active}"
> >
<Settings size={20} class="carbon-icon dark:text-white" /> <Settings size={20} class="carbon-icon dark:text-white" />
<p class="dark:text-white ml-2">Settings</p> <p class="dark:text-white ml-2">{$t('lms_navigation.settings')}</p>
</li> </li>
</a> </a>
</ul> </ul>
@@ -4,6 +4,7 @@
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte'; import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte';
import { coursesInProgress } from '$lib/components/Courses/store'; import { coursesInProgress } from '$lib/components/Courses/store';
import { t } from '$lib/utils/functions/translations';
const gotoCourse = (id: string | undefined) => { const gotoCourse = (id: string | undefined) => {
if (!id) return; if (!id) return;
@@ -12,7 +13,9 @@
</script> </script>
<section class="h-full"> <section class="h-full">
<p class="text-base font-semibold text-[#040F2D] pb-3 dark:text-white">Currently Learning</p> <p class="text-base font-semibold text-[#040F2D] pb-3 dark:text-white">
{$t('dashboard.current_lesson')}
</p>
<div <div
class="flex items-center flex-col border border-[#EAEAEA] dark:bg-neutral-800 gap-2 rounded w-full lg:h-[40vh] p-3" class="flex items-center flex-col border border-[#EAEAEA] dark:bg-neutral-800 gap-2 rounded w-full lg:h-[40vh] p-3"
> >
@@ -33,7 +36,7 @@
</p> </p>
</div> </div>
<PrimaryButton <PrimaryButton
label="Continue" label={$t('dashboard.continue')}
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
className="rounded-none text-[#0233BD]" className="rounded-none text-[#0233BD]"
onClick={() => gotoCourse(course.id)} onClick={() => gotoCourse(course.id)}
@@ -53,8 +56,8 @@
{:else} {:else}
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<CoursesEmptyIcon /> <CoursesEmptyIcon />
<h3 class="text-center">No courses in progress yet</h3> <h3 class="text-center">{$t('dashboard.no_courses')}</h3>
<p class="w-[75%] text-center">Once you start a course, your progress will reflect here</p> <p class="w-[75%] text-center">{$t('dashboard.start_course')}</p>
</div> </div>
{/if} {/if}
</div> </div>
@@ -9,9 +9,14 @@
import { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
import LogoutButton from '$lib/components/Buttons/Logout/index.svelte'; import LogoutButton from '$lib/components/Buttons/Logout/index.svelte';
import generateUUID from '$lib/utils/functions/generateUUID'; import generateUUID from '$lib/utils/functions/generateUUID';
import { t } from '$lib/utils/functions/translations';
import LanguagePicker from '$lib/components/Org/Settings/LanguagePicker.svelte';
import { handleLocaleChange } from '$lib/utils/functions/translations';
let avatar = ''; let avatar = '';
let loading = false; let loading = false;
let hasLangChanged = false;
let locale = '';
async function handleUpdate() { async function handleUpdate() {
try { try {
@@ -20,7 +25,8 @@
const updates = { const updates = {
fullname: $profile.fullname, fullname: $profile.fullname,
username: $profile.username, username: $profile.username,
email: $profile.email email: $profile.email,
locale
}; };
if (avatar) { if (avatar) {
@@ -47,25 +53,31 @@
})); }));
snackbar.success(); snackbar.success();
if (hasLangChanged) {
handleLocaleChange(locale);
}
if (error) throw error; if (error) throw error;
} catch (error) { } catch (error) {
let message = error.message; let message = error.message;
if (message.includes('profile_username_key')) { if (message.includes('profile_username_key')) {
message = 'username already exists'; message = $t('username already exists');
} }
snackbar.error(`Update failed: ${message}`); snackbar.error(`snackbar.lms.error.update: ${message}`);
loading = false; loading = false;
} finally { } finally {
loading = false; loading = false;
} }
} }
$: locale = $profile.locale;
</script> </script>
<Grid class="border-c rounded border-gray-200 dark:border-neutral-600 w-full mt-5"> <Grid class="border-c rounded border-gray-200 dark:border-neutral-600 w-full mt-5">
<Row class="flex flex-col lg:flex-row items-center lg:items-start py-7 border-bottom-c "> <Row class="flex flex-col lg:flex-row items-center lg:items-start py-7 border-bottom-c ">
<Column sm={4} md={8} lg={4} class="mt-2 md:mt-0"> <Column sm={4} md={8} lg={4} class="mt-2 md:mt-0">
<p class="dark:text-white font-bold">Profile Picture</p> <p class="dark:text-white font-bold">{$t('settings.profile.profile_picture.heading')}</p>
</Column> </Column>
<Column sm={2} md={4} lg={8} class="mt-2 lg:mt-0"> <Column sm={2} md={4} lg={8} class="mt-2 lg:mt-0">
<UploadImage bind:avatar src={$profile.avatar_url} widthHeight="w-16 h-16 lg:w-24 lg:h-24" /> <UploadImage bind:avatar src={$profile.avatar_url} widthHeight="w-16 h-16 lg:w-24 lg:h-24" />
@@ -73,17 +85,35 @@
</Row> </Row>
<Row class="flex flex-col lg:flex-row py-7 border-bottom-c"> <Row class="flex flex-col lg:flex-row py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}> <Column sm={4} md={4} lg={4}>
<p class="dark:text-white font-bold">Personal Information</p> <p class="dark:text-white font-bold">{$t('settings.profile.personal_information.heading')}</p>
</Column> </Column>
<Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0"> <Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0">
<TextField label="Full Name" bind:value={$profile.fullname} className="w-full lg:w-60 mb-4" /> <TextField
<TextField label="Username" bind:value={$profile.username} className="w-full lg:w-60 mb-4" /> label={$t('settings.profile.personal_information.full_name')}
<TextField label="Email" bind:value={$profile.email} className="w-full lg:w-60 mb-4" /> bind:value={$profile.fullname}
className="w-full lg:w-60 mb-4"
/>
<TextField
label={$t('settings.profile.personal_information.username')}
bind:value={$profile.username}
className="w-full lg:w-60 mb-4"
/>
<TextField
label={$t('settings.profile.personal_information.email')}
bind:value={$profile.email}
className="w-full lg:w-60 mb-4"
/>
<LanguagePicker
bind:hasLangChanged
bind:value={$profile.locale}
className="w-full lg:w-60 mb-4"
/>
</Column> </Column>
</Row> </Row>
<Row class="m-5 w-full flex items-center gap-2 lg:justify-center"> <Row class="m-5 w-full flex items-center gap-2 lg:justify-center">
<PrimaryButton <PrimaryButton
label="Update Profile" label={$t('settings.profile.update_profile')}
variant={VARIANTS.CONTAINED_DARK} variant={VARIANTS.CONTAINED_DARK}
className="mr-5" className="mr-5"
isLoading={loading} isLoading={loading}
@@ -4,6 +4,7 @@
import { isMobile } from '$lib/utils/store/useMobile'; import { isMobile } from '$lib/utils/store/useMobile';
import { formatDate } from '$lib/utils/functions/routes/dashboard'; import { formatDate } from '$lib/utils/functions/routes/dashboard';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { t } from '$lib/utils/functions/translations';
let store; let store;
@@ -26,7 +27,7 @@
</script> </script>
<section class="w-full h-full mt-2"> <section class="w-full h-full mt-2">
<p class="text-base font-semibold text-[#040F2D] pb-3 dark:text-white">Your Schedule</p> <p class="text-base font-semibold text-[#040F2D] pb-3 dark:text-white">{$t('schedule.title')}</p>
<div <div
class="flex items-center flex-col border border-[#EAEAEA] dark:bg-neutral-800 rounded w-full h-fit lg:h-[40vh] p-3 overflow-y-auto" class="flex items-center flex-col border border-[#EAEAEA] dark:bg-neutral-800 rounded w-full h-fit lg:h-[40vh] p-3 overflow-y-auto"
@@ -51,23 +52,23 @@
{formatDate($store?.selected)} {formatDate($store?.selected)}
</p> </p>
{/if} {/if}
{#if formatDate($store?.selected) == 'Today'} {#if formatDate($store?.selected) == $t('dashboard.today')}
<span class="flex flex-col md:flex-row items-baseline justify-center gap-10 mt-5"> <span class="flex flex-col md:flex-row items-baseline justify-center gap-10 mt-5">
<p class="text-xs font-normal text-[#656565] dark:text-white">2:30pm</p> <p class="text-xs font-normal text-[#656565] dark:text-white">2:30pm</p>
<div class="flex flex-col items-start gap-4"> <div class="flex flex-col items-start gap-4">
<span> <span>
<p class="text-lg font-semibold text-[#262626] dark:text-white"> <p class="text-lg font-semibold text-[#262626] dark:text-white">
The essence of design collaboration {$t('schedule.essence')}
</p> </p>
<a href="#" class="text-sm font-normal text-primary-700 dark:text-white" <a href="#" class="text-sm font-normal text-primary-700 dark:text-white"
>Figma Introduction</a >{$t('schedule.figma')}</a
> >
</span> </span>
<PrimaryButton label="Join Call" variant={VARIANTS.OUTLINED} className="" /> <PrimaryButton label={$t('schedule.join')} variant={VARIANTS.OUTLINED} className="" />
</div> </div>
</span> </span>
{:else} {:else}
<p>No Lesson on this day</p> <p>{$t('schedule.no')}</p>
{/if} {/if}
</div> </div>
</section> </section>
@@ -1,4 +1,6 @@
<script> <script>
import { t } from '$lib/utils/functions/translations';
export let value; export let value;
export let placeholder = ``; export let placeholder = ``;
const activeClassName = 'border-b-2 border-primary-700'; const activeClassName = 'border-b-2 border-primary-700';
@@ -15,13 +17,13 @@
on:click={handleTabClick(1)} on:click={handleTabClick(1)}
class="focus:outline-none p-2 {isWriteMode && `${activeClassName}`}" class="focus:outline-none p-2 {isWriteMode && `${activeClassName}`}"
> >
Write {$t('markdown_editor.write')}
</button> </button>
<button <button
on:click={handleTabClick(2)} on:click={handleTabClick(2)}
class="focus:outline-none p-2 {!isWriteMode && `${activeClassName}`}" class="focus:outline-none p-2 {!isWriteMode && `${activeClassName}`}"
> >
Preview {$t('markdown_editor.preview')}
</button> </button>
</div> </div>
@@ -13,6 +13,7 @@
import { currentOrgPath } from '$lib/utils/store/org'; import { currentOrgPath } from '$lib/utils/store/org';
import { toggleBodyByMode } from '$lib/utils/functions/app'; import { toggleBodyByMode } from '$lib/utils/functions/app';
import { sideBar } from '../Org/store'; import { sideBar } from '../Org/store';
import { t } from '$lib/utils/functions/translations';
export let title = ''; export let title = '';
export let navClass = ''; export let navClass = '';
@@ -76,7 +77,9 @@
{/if} {/if}
<a <a
href={coursesPath} href={coursesPath}
title="Go to {isCoursePage ? 'Courses' : 'ClassroomIO Home'}" title="{$t('navigation.goto')} {isCoursePage
? $t('navigation.courses')
: $t('navigation.classroomio_home')}"
id="logo" id="logo"
class="text-lg line-clamp-1" class="text-lg line-clamp-1"
> >
@@ -5,6 +5,7 @@
import { isCoursePage } from '$lib/utils/functions/app'; import { isCoursePage } from '$lib/utils/functions/app';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { t } from '$lib/utils/functions/translations';
export let disableSignup = false; export let disableSignup = false;
export let logo; export let logo;
@@ -24,7 +25,12 @@
> >
<ul class="flex w-full items-center"> <ul class="flex w-full items-center">
<div class="logo"> <div class="logo">
<a href="/" title={`Go to ${orgName || 'ClassroomIO'} Home`} id="logo" data-hveid="8"> <a
href="/"
title={`${$t('navigation.goto')} ${orgName || 'ClassroomIO'} ${$t('navigation.home')}`}
id="logo"
data-hveid="8"
>
<img <img
src={logo || '/logo-192.png'} src={logo || '/logo-192.png'}
alt={`${orgName || 'ClassroomIO'} logo`} alt={`${orgName || 'ClassroomIO'} logo`}
@@ -36,19 +42,19 @@
{#if $user.isLoggedIn} {#if $user.isLoggedIn}
<li class="hidden"> <li class="hidden">
<a class="block" href="dashboard"> Dashboard </a> <a class="block" href="dashboard"> {$t('navigation.dashboard')} </a>
</li> </li>
<li class="hidden"> <li class="hidden">
<a class="block" href="courses"> Courses </a> <a class="block" href="courses"> {$t('navigation.courses')} </a>
</li> </li>
<li class="hidden"> <li class="hidden">
<a class="block" href="discussions"> Discussion </a> <a class="block" href="discussions"> {$t('navigation.discussion')} </a>
</li> </li>
<li class="hidden"> <li class="hidden">
<a class="block" href="people">People </a> <a class="block" href="people">{$t('navigation.people')} </a>
</li> </li>
{/if} {/if}
@@ -56,13 +62,13 @@
{#if $user.isLoggedIn} {#if $user.isLoggedIn}
{#if isOrgSite} {#if isOrgSite}
<li><a class="block" href="/lms"> Go to LMS </a></li> <li><a class="block" href="/lms"> {$t('navigation.goto_lms')} </a></li>
{/if} {/if}
{:else if !$page.url.pathname?.includes('/404')} {:else if !$page.url.pathname?.includes('/404')}
<li> <li>
<div class="flex"> <div class="flex">
<PrimaryButton <PrimaryButton
label="Login" label={$t('navigation.login')}
variant={VARIANTS.TEXT} variant={VARIANTS.TEXT}
onClick={() => goto('/login' + redirect)} onClick={() => goto('/login' + redirect)}
/> />
@@ -72,7 +78,7 @@
{#if !disableSignup} {#if !disableSignup}
<div class="flex"> <div class="flex">
<PrimaryButton <PrimaryButton
label="Signup" label={$t('navigation.signup')}
variant={VARIANTS.CONTAINED} variant={VARIANTS.CONTAINED}
onClick={() => goto('/signup' + redirect)} onClick={() => goto('/signup' + redirect)}
/> />
@@ -13,6 +13,7 @@
import { sideBar } from '$lib/components/Org/store'; import { sideBar } from '$lib/components/Org/store';
import { currentOrg } from '$lib/utils/store/org'; import { currentOrg } from '$lib/utils/store/org';
import { toggleBodyByMode } from '$lib/utils/functions/app'; import { toggleBodyByMode } from '$lib/utils/functions/app';
import { t } from '$lib/utils/functions/translations';
export let navClass = ''; export let navClass = '';
@@ -48,7 +49,7 @@
<div class=""> <div class="">
<a <a
href={$page.url.pathname} href={$page.url.pathname}
title="Go to ClassroomIO Home" title={$t('navigation.goto_home')}
id="logo" id="logo"
class="text-lg flex items-center" class="text-lg flex items-center"
> >
@@ -11,6 +11,7 @@
import { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
import { createOrgValidation } from '$lib/utils/functions/validator'; import { createOrgValidation } from '$lib/utils/functions/validator';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { t } from '$lib/utils/functions/translations';
type Error = { type Error = {
orgName: string; orgName: string;
@@ -84,8 +85,7 @@
if (error) { if (error) {
console.log('Error: create organisation member', error); console.log('Error: create organisation member', error);
errors.siteName = errors.siteName = $t('add_org.error_organization');
'Something went wrong while creating this organization. Please reload and try again';
// Delete organization so it can be recreated. // Delete organization so it can be recreated.
await supabase.from('organization').delete().match({ siteName }); await supabase.from('organization').delete().match({ siteName });
@@ -109,11 +109,11 @@
onClose={() => ($newOrgModal.open = false)} onClose={() => ($newOrgModal.open = false)}
bind:open={$newOrgModal.open} bind:open={$newOrgModal.open}
width="w-96" width="w-96"
modalHeading="Create Organization" modalHeading={$t('add_org.create_org')}
> >
<form on:submit|preventDefault={createNewOrg} class="px-2"> <form on:submit|preventDefault={createNewOrg} class="px-2">
<TextField <TextField
label="Name of Organization" label={$t('add_org.name')}
bind:value={orgName} bind:value={orgName}
autoFocus={true} autoFocus={true}
placeholder="e.g Pepsi Co" placeholder="e.g Pepsi Co"
@@ -124,7 +124,7 @@
/> />
<!-- Org Site Name --> <!-- Org Site Name -->
<TextField <TextField
label="Organisation Site name" label={$t('add_org.org_sitename')}
helperMessage={`https://${siteName || ''}.classroomio.com`} helperMessage={`https://${siteName || ''}.classroomio.com`}
bind:value={siteName} bind:value={siteName}
name="sitename" name="sitename"
@@ -137,7 +137,12 @@
/> />
<div class="mt-5 flex items-center flex-row-reverse"> <div class="mt-5 flex items-center flex-row-reverse">
<PrimaryButton className="px-6 py-3" label="Create" type="submit" isLoading={loading} /> <PrimaryButton
className="px-6 py-3"
label={$t('add_org.create')}
type="submit"
isLoading={loading}
/>
</div> </div>
</form> </form>
</Modal> </Modal>
@@ -5,16 +5,17 @@
ToolbarContent, ToolbarContent,
ToolbarSearch, ToolbarSearch,
Pagination, Pagination,
DataTableSkeleton, DataTableSkeleton
} from 'carbon-components-svelte'; } from 'carbon-components-svelte';
import { orgAudience } from '$lib/utils/store/org'; import { orgAudience } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations';
export let isLoading = false; export let isLoading = false;
const headers = [ const headers = [
{ key: 'name', value: 'Name' }, { key: 'name', value: $t('audience.name') },
{ key: 'email', value: 'Email' }, { key: 'email', value: $t('audience.email') },
{ key: 'date_joined', value: 'Date Joined' }, { key: 'date_joined', value: $t('audience.date_joined') }
]; ];
let pageSize = 5; let pageSize = 5;
let page = 1; let page = 1;
@@ -28,20 +29,10 @@
<DataTable size="tall" {headers} rows={$orgAudience} {pageSize} {page}> <DataTable size="tall" {headers} rows={$orgAudience} {pageSize} {page}>
<Toolbar> <Toolbar>
<ToolbarContent> <ToolbarContent>
<ToolbarSearch <ToolbarSearch persistent value={searchValue} shouldFilterRows bind:filteredRowIds />
persistent
value={searchValue}
shouldFilterRows
bind:filteredRowIds
/>
</ToolbarContent> </ToolbarContent>
</Toolbar> </Toolbar>
</DataTable> </DataTable>
{/if} {/if}
<Pagination <Pagination bind:pageSize bind:page totalItems={filteredRowIds.length} pageSizeInputDisabled />
bind:pageSize
bind:page
totalItems={filteredRowIds.length}
pageSizeInputDisabled
/>
@@ -4,6 +4,7 @@
import AudienceEmptyIcon from '$lib/components/Icons/AudienceEmptyIcon.svelte'; import AudienceEmptyIcon from '$lib/components/Icons/AudienceEmptyIcon.svelte';
import { getOrgAudience } from '$lib/utils/services/org'; import { getOrgAudience } from '$lib/utils/services/org';
import { orgAudience, currentOrg } from '$lib/utils/store/org'; import { orgAudience, currentOrg } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations';
let isLoading = false; let isLoading = false;
@@ -24,9 +25,9 @@
{:else} {:else}
<Box> <Box>
<AudienceEmptyIcon /> <AudienceEmptyIcon />
<h3 class="dark:text-white text-2xl text-center my-5">No audience to manage!</h3> <h3 class="dark:text-white text-2xl text-center my-5">{$t('audience.no_audience')}!</h3>
<p class="dark:text-white w-1/3 text-center"> <p class="dark:text-white w-1/3 text-center">
Manage all your students here and stay connected with them. {$t('audience.manage')}.
</p> </p>
</Box> </Box>
{/if} {/if}
@@ -2,6 +2,7 @@
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 { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { t } from '$lib/utils/functions/translations';
export let onDelete = () => {}; export let onDelete = () => {};
export let onCancel = () => {}; export let onCancel = () => {};
@@ -14,24 +15,27 @@
onClose={() => (open = false)} onClose={() => (open = false)}
bind:open bind:open
width="w-96" width="w-96"
modalHeading="Delete {isQuestion ? 'question' : 'comment'}" modalHeading="{$t('community.delete.title')} {isQuestion
? $t('community.delete.question')
: $t('community.delete.comment')}"
> >
<div> <div>
<h1 class="dark:text-white text-lg"> <h1 class="dark:text-white text-lg">
Are you sure you want to delete this {isQuestion ? 'question' : 'comment'}? {$t('community.delete.sure')}
{isQuestion ? $t('community.delete.question') : $t('community.delete.comment')}?
</h1> </h1>
<div class="mt-5 flex items-center justify-between"> <div class="mt-5 flex items-center justify-between">
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="No, keep it" label={$t('community.delete.no')}
onClick={onCancel} onClick={onCancel}
/> />
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
variant={VARIANTS.CONTAINED} variant={VARIANTS.CONTAINED}
label="Yes, delete" label={$t('community.delete.yes')}
onClick={onDelete} onClick={onDelete}
isLoading={isDeleting} isLoading={isDeleting}
/> />
@@ -12,6 +12,7 @@
import { fetchCourses } from '$lib/components/Courses/api'; import { fetchCourses } from '$lib/components/Courses/api';
import { profile } from '$lib/utils/store/user'; import { profile } from '$lib/utils/store/user';
import { Search, Dropdown } from 'carbon-components-svelte'; import { Search, Dropdown } from 'carbon-components-svelte';
import { t } from '$lib/utils/functions/translations';
export let isLMS = false; export let isLMS = false;
@@ -85,15 +86,15 @@
<div class="flex justify-between"> <div class="flex justify-between">
<Search <Search
placeholder="Find Question" placeholder={$t('community.find_question')}
bind:value={searchValue} bind:value={searchValue}
searchClass="mr-2" searchClass="mr-2"
class=" bg-gray-100 dark:bg-neutral-800" class=" bg-gray-100 dark:bg-neutral-800"
/> />
<Dropdown <Dropdown
class="w-[25%]" class="w-[25%] h-full"
size="xl" size="xl"
label="Select Course" label={$t('community.ask.select_course')}
items={allCourses.map((course) => ({ id: course.id, text: course.title }))} items={allCourses.map((course) => ({ id: course.id, text: course.title }))}
bind:selectedId bind:selectedId
/> />
@@ -137,8 +138,8 @@
{:else} {:else}
<Box className="w-screen"> <Box className="w-screen">
<CoursesEmptyIcon /> <CoursesEmptyIcon />
<h3 class="dark:text-white text-2xl my-5">No Questions asked</h3> <h3 class="dark:text-white text-2xl my-5">{$t('community.no_question')}</h3>
<p class="dark:text-white w-1/3 text-center">Ask a question to the community</p> <p class="dark:text-white w-1/3 text-center">{$t('community.ask_a_question')}</p>
</Box> </Box>
{/each} {/each}
{/if} {/if}
@@ -1,5 +1,6 @@
<script> <script>
import { Search } from 'carbon-components-svelte'; import { Search } from 'carbon-components-svelte';
import { t } from '$lib/utils/functions/translations';
let searchValue = ''; let searchValue = '';
</script> </script>
@@ -9,13 +10,16 @@
<!-- Course Description --> <!-- Course Description -->
<div class="course-descr w-11/12 py-10"> <div class="course-descr w-11/12 py-10">
<h1 class="dark:text-white text-5xl text-white text-center my-4 font-bold"> <h1 class="dark:text-white text-5xl text-white text-center my-4 font-bold">
Explore courses {$t('course.navItem.landing_page.explore')}
</h1> </h1>
<p class="dark:text-white text-md text-white mb-6 text-center"> <p class="dark:text-white text-md text-white mb-6 text-center">
Find courses you will love from best teachers all over the world🌎. {$t('course.navItem.landing_page.find')}🌎.
</p> </p>
<Search placeholder="Find a course..." bind:value={searchValue} /> <Search
placeholder={$t('course.navItem.landing_page.find_course')}
bind:value={searchValue}
/>
</div> </div>
</div> </div>
</section> </section>
@@ -27,6 +27,7 @@
import TextArea from '$lib/components/Form/TextArea.svelte'; import TextArea from '$lib/components/Form/TextArea.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { landingPageSettings } from '$lib/components/Org/Settings/store'; import { landingPageSettings } from '$lib/components/Org/Settings/store';
import { t } from '$lib/utils/functions/translations';
import PoweredBy from '$lib/components/Upgrade/PoweredBy.svelte'; import PoweredBy from '$lib/components/Upgrade/PoweredBy.svelte';
export let orgSiteName = ''; export let orgSiteName = '';
@@ -148,7 +149,7 @@
<svelte:head> <svelte:head>
<title> <title>
{!org.name ? '' : `${org.name}'s `}Landing Page {!org.name ? '' : `${org.name}'s `}{$t('course.navItem.landing_page.landing_page')}
</title> </title>
</svelte:head> </svelte:head>
@@ -198,7 +199,7 @@
{#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
title="Header video" title={$t('course.navItem.landing_page.header_video')}
src={$landingPageSettings.header?.banner?.video} src={$landingPageSettings.header?.banner?.video}
allowfullscreen allowfullscreen
allowtransparency allowtransparency
@@ -311,9 +312,11 @@
{:else} {:else}
<Box> <Box>
<CoursesEmptyIcon /> <CoursesEmptyIcon />
<h3 class="dark:text-white text-2xl my-5">No Course Published</h3> <h3 class="dark:text-white text-2xl my-5">
{$t('course.navItem.landing_page.no_course_published')}
</h3>
<p class="dark:text-white w-1/3 text-center"> <p class="dark:text-white w-1/3 text-center">
We've got great courses coming your way, stay tuned!!! {$t('course.navItem.landing_page.coming_your_way')}
</p> </p>
</Box> </Box>
{/if} {/if}
@@ -323,7 +326,9 @@
<PrimaryButton <PrimaryButton
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
onClick={() => (viewAll = !viewAll)} onClick={() => (viewAll = !viewAll)}
label={viewAll ? 'View Less' : 'View All'} label={viewAll
? $t('course.navItem.landing_page.view_less')
: $t('course.navItem.landing_page.view_all')}
className="px-10 py-5 w-fit" className="px-10 py-5 w-fit"
/> />
</div> </div>
@@ -393,14 +398,14 @@
<div class="mt-8 bg-white p-7 rounded-lg"> <div class="mt-8 bg-white p-7 rounded-lg">
{#if successContactSaved} {#if successContactSaved}
<div class="w-full flex items-center justify-center"> <div class="w-full flex items-center justify-center">
Thank you for dropping a message, we will get back to you shortly {$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="w-full flex justify-between flex-col md:flex-row">
<div class="w-full md:w-2/4 mr-5"> <div class="w-full md:w-2/4 mr-5">
<TextField <TextField
label="Your Name" label={$t('course.navItem.landing_page.name')}
bind:value={contact.name} bind:value={contact.name}
errorMessage={contactError.name} errorMessage={contactError.name}
className="mb-5" className="mb-5"
@@ -408,14 +413,14 @@
placeholder="Elon Musk" placeholder="Elon Musk"
/> />
<TextField <TextField
label="Your Email" label={$t('course.navItem.landing_page.email')}
bind:value={contact.email} bind:value={contact.email}
errorMessage={contactError.email} errorMessage={contactError.email}
className="text-xs font-normal mb-5" className="text-xs font-normal mb-5"
placeholder="[email protected]" placeholder="[email protected]"
/> />
<TextField <TextField
label="Your Phone" label={$t('course.navItem.landing_page.phone')}
bind:value={contact.phone} bind:value={contact.phone}
errorMessage={contactError.phone} errorMessage={contactError.phone}
className="text-xs font-normal mb-5" className="text-xs font-normal mb-5"
@@ -424,12 +429,12 @@
</div> </div>
<div class="w-full md:w-2/4"> <div class="w-full md:w-2/4">
<TextArea <TextArea
label="Your Message" label={$t('course.navItem.landing_page.message')}
bind:value={contact.message} bind:value={contact.message}
errorMessage={contactError.message} errorMessage={contactError.message}
rows="9" rows="9"
maxRows={15} maxRows={15}
placeholder="Your message here" placeholder={$t('course.navItem.landing_page.your_message')}
/> />
</div> </div>
</div> </div>
@@ -439,7 +444,7 @@
type="submit" type="submit"
isLoading={isContactSubmiting} isLoading={isContactSubmiting}
> >
<span class="mr-2 text-md">Submit</span> <span class="mr-2 text-md">{$t('course.navItem.landing_page.submit')}</span>
<Rocket size={24} /> <Rocket size={24} />
</PrimaryButton> </PrimaryButton>
</form> </form>
@@ -467,12 +472,12 @@
<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 items-center flex-col sm:flex-row">
{#if success} {#if success}
<p class="text-white">You have been added successfully. Thanks for subscribing.</p> <p class="text-white">{$t('course.navItem.landing_page.successful_sub')}</p>
{:else} {:else}
<TextField <TextField
bind:value={email} bind:value={email}
type="email" type="email"
placeholder="Enter your email.." placeholder={$t('course.navItem.landing_page.enter')}
className="sm:mr-3 my-0 w-full md:w-fit" className="sm:mr-3 my-0 w-full md:w-fit"
isRequired={true} isRequired={true}
isDisabled={isAdding} isDisabled={isAdding}
@@ -525,7 +530,7 @@
<a <a
href={$landingPageSettings.footer.facebook} href={$landingPageSettings.footer.facebook}
target="_blank" target="_blank"
title="Facebook" title={$t('settings.landing_page.footer.facebook')}
id="logo-fb" id="logo-fb"
data-hveid="8" data-hveid="8"
> >
@@ -538,7 +543,7 @@
<a <a
href={$landingPageSettings.footer.twitter} href={$landingPageSettings.footer.twitter}
target="_blank" target="_blank"
title="Twitter" title={$t('settings.landing_page.footer.twitter')}
id="logo-tw" id="logo-tw"
data-hveid="8" data-hveid="8"
> >
@@ -552,7 +557,7 @@
<a <a
href={$landingPageSettings.footer.linkedin} href={$landingPageSettings.footer.linkedin}
target="_blank" target="_blank"
title="Linkedin" title={$t('settings.landing_page.footer.linkedin')}
id="logo-ln" id="logo-ln"
data-hveid="8" data-hveid="8"
> >
@@ -4,6 +4,7 @@
import Modal from '$lib/components/Modal/index.svelte'; import Modal from '$lib/components/Modal/index.svelte';
import { deleteModal, quizesStore } from '$lib/utils/store/org'; import { deleteModal, quizesStore } from '$lib/utils/store/org';
import { supabase } from '$lib/utils/functions/supabase'; import { supabase } from '$lib/utils/functions/supabase';
import { t } from '$lib/utils/functions/translations';
export let onDelete = () => {}; export let onDelete = () => {};
@@ -34,24 +35,26 @@
onClose={closeModal} onClose={closeModal}
bind:open={$deleteModal.open} bind:open={$deleteModal.open}
width="w-2/5" width="w-2/5"
modalHeading={$deleteModal.isQuestion ? 'Delete Question' : 'Delete Quiz'} modalHeading={$deleteModal.isQuestion
? $t('components.quiz.question_delete')
: $t('components.quiz.quiz_delete')}
size="sm" size="sm"
> >
<div class="w-full flex flex-col items-center my-10"> <div class="w-full flex flex-col items-center my-10">
<h1 class="dark:text-white text-lg text-center"> <h1 class="dark:text-white text-lg text-center">
{#if $deleteModal.isQuestion} {#if $deleteModal.isQuestion}
Are you sure you want to delete this question? {$t('components.quiz.delete_question')}
{:else} {:else}
Are you sure you want to delete this quiz? {$t('components.quiz.delete_quiz')}
{/if} {/if}
You will lose your content and this action can’t be undone. {$t('components.quiz.lose_content')}
</h1> </h1>
<div class="mt-5"> <div class="mt-5">
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
variant={VARIANTS.CONTAINED} variant={VARIANTS.CONTAINED}
label="Delete" label={$t('components.quiz.delete')}
onClick={() => { onClick={() => {
if ($deleteModal.isQuestion) { if ($deleteModal.isQuestion) {
onDelete(); onDelete();
@@ -7,6 +7,7 @@
import { supabase } from '$lib/utils/functions/supabase'; import { supabase } from '$lib/utils/functions/supabase';
import { currentOrg, createQuizModal, currentOrgPath, quizesStore } from '$lib/utils/store/org'; import { currentOrg, createQuizModal, currentOrgPath, quizesStore } from '$lib/utils/store/org';
import { createQuizValidation } from '$lib/utils/functions/validator'; import { createQuizValidation } from '$lib/utils/functions/validator';
import { t } from '$lib/utils/functions/translations';
let open = false; let open = false;
let errors = { let errors = {
@@ -97,21 +98,23 @@
</script> </script>
<svelte:head> <svelte:head>
<title>Quizzes - ClassroomIO</title> <title>{$t('components.quiz.title')} - ClassroomIO</title>
</svelte:head> </svelte:head>
<Modal <Modal
onClose={handleClose} onClose={handleClose}
bind:open bind:open
width="w-4/5 md:w-2/5" width="w-4/5 md:w-2/5"
modalHeading="{$createQuizModal.openEdit ? 'Update' : 'Create'} a Quiz" modalHeading="{$createQuizModal.openEdit
? $t('components.quiz.update')
: $t('components.quiz.create')} {$t('components.quiz.a_quiz')}"
> >
<form on:submit|preventDefault={createQuiz}> <form on:submit|preventDefault={createQuiz}>
<TextField <TextField
label="Quiz Title" label={$t('components.quiz.quiz_title')}
bind:value={$createQuizModal.title} bind:value={$createQuizModal.title}
autofocus={true} autofocus={true}
placeholder="Your quiz title" placeholder={$t('components.quiz.placehoolder')}
className="mb-4" className="mb-4"
isRequired={true} isRequired={true}
errorMessage={errors.title} errorMessage={errors.title}
@@ -121,7 +124,9 @@
<div class="mt-5 flex items-center justify-end"> <div class="mt-5 flex items-center justify-end">
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
label={$createQuizModal.openEdit ? 'Save Changes' : 'Continue'} label={$createQuizModal.openEdit
? $t('components.quiz.save')
: $t('components.quiz.continue')}
type="submit" type="submit"
isDisabled={isLoading} isDisabled={isLoading}
{isLoading} {isLoading}
@@ -9,6 +9,7 @@
import { onRename, onDelete } from '$lib/utils/services/org/quiz'; import { onRename, onDelete } from '$lib/utils/services/org/quiz';
import { themeImages } from '$lib/utils/constants/quiz'; import { themeImages } from '$lib/utils/constants/quiz';
import { calDateDiff } from '$lib/utils/functions/date'; import { calDateDiff } from '$lib/utils/functions/date';
import { t } from '$lib/utils/functions/translations';
export let quiz; export let quiz;
export let totalQuestions; export let totalQuestions;
@@ -28,7 +29,7 @@
// onClick: onReport(quiz), // onClick: onReport(quiz),
// }, // },
{ {
label: 'Rename', label: $t('components.quiz.rename'),
onClick() { onClick() {
onRename(quiz); onRename(quiz);
} }
@@ -40,7 +41,7 @@
// }, // },
// }, // },
{ {
label: 'Delete', label: $t('components.quiz.delete'),
onClick() { onClick() {
onDelete(quiz); onDelete(quiz);
} }
@@ -77,13 +78,13 @@
<div class="flex md:flex-row flex-col justify-between"> <div class="flex md:flex-row flex-col justify-between">
<div> <div>
<p class="mb-2">{pluralize('question', totalQuestions, true)}</p> <p class="mb-2">{pluralize('question', totalQuestions, true)}</p>
<p class="mb-2 md:mb-0">Updated {calDateDiff(quiz.updated_at)}</p> <p class="mb-2 md:mb-0">{$t('components.quiz.updated')} {calDateDiff(quiz.updated_at)}</p>
</div> </div>
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="Start Quiz" label={$t('components.quiz.start')}
onClick={startQuiz} onClick={startQuiz}
/> />
</div> </div>
@@ -4,6 +4,7 @@
import { optionImage } from '$lib/utils/constants/quiz'; import { optionImage } from '$lib/utils/constants/quiz';
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import IconButton from '$lib/components/IconButton/index.svelte'; import IconButton from '$lib/components/IconButton/index.svelte';
import { t } from '$lib/utils/functions/translations';
export let currentQuestion = { export let currentQuestion = {
label: '' label: ''
@@ -17,10 +18,10 @@
<h1 class="text-white mb-5 font-bold">{currentQuestion.label}</h1> <h1 class="text-white mb-5 font-bold">{currentQuestion.label}</h1>
{:else} {:else}
<TextField <TextField
placeholder="Start typing your question" placeholder={$t('components.quiz.start_typing')}
bind:value={currentQuestion.label} bind:value={currentQuestion.label}
className="w-full my-4" className="w-full my-4"
errorMessage={currentError.isLabelEmpty && 'This field is required'} errorMessage={currentError.isLabelEmpty && $t('components.quiz.required_field')}
isRequired={true} isRequired={true}
bgColor="bg-white" bgColor="bg-white"
/> />
@@ -37,11 +38,12 @@
{:else} {:else}
<TextField <TextField
bind:value={option.label} bind:value={option.label}
placeholder="Type answer" placeholder={$t('components.quiz.type_answer')}
bgColor="bg-white" bgColor="bg-white"
className="mt-3" className="mt-3"
isDisabled={currentQuestion.type === 'boolean'} isDisabled={currentQuestion.type === 'boolean'}
errorMessage={optionHasError(option.id, currentError.options) && 'Please enter a label'} errorMessage={optionHasError(option.id, currentError.options) &&
$t('components.quiz.label')}
/> />
<div class="flex justify-end absolute top-2 right-2"> <div class="flex justify-end absolute top-2 right-2">
<IconButton <IconButton
@@ -7,6 +7,7 @@
import { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
import { supabase } from '$lib/utils/functions/supabase'; import { supabase } from '$lib/utils/functions/supabase';
import { currentOrg, quizesStore } from '$lib/utils/store/org'; import { currentOrg, quizesStore } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations';
let isLoading = false; let isLoading = false;
@@ -43,9 +44,9 @@
{:else} {:else}
<Box> <Box>
<CoursesEmptyIcon /> <CoursesEmptyIcon />
<h3 class="dark:text-white text-2xl my-5">No Quiz Created</h3> <h3 class="dark:text-white text-2xl my-5">{$t('components.quiz.no_quizz')}</h3>
<p class="dark:text-white w-1/3 text-center"> <p class="dark:text-white w-1/3 text-center">
Create interactive quizzes with scoreboard for your students. {$t('components.quiz.interactive')}
</p> </p>
</Box> </Box>
{/each} {/each}
@@ -4,6 +4,7 @@
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { currentOrgPlan, isFreePlan } from '$lib/utils/store/org'; import { currentOrgPlan, isFreePlan } from '$lib/utils/store/org';
import SectionTitle from '../SectionTitle.svelte'; import SectionTitle from '../SectionTitle.svelte';
import { t } from '$lib/utils/functions/translations';
let isRedirecting = false; let isRedirecting = false;
@@ -26,16 +27,18 @@
<Grid class="border-c rounded border-gray-200 dark:border-neutral-600 w-full mt-5"> <Grid class="border-c rounded border-gray-200 dark:border-neutral-600 w-full mt-5">
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}><SectionTitle>Billing details</SectionTitle></Column> <Column sm={4} md={4} lg={4}
><SectionTitle>{$t('settings.billing.sub_title')}</SectionTitle></Column
>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<h4 class="dark:text-white lg:mt-0">Manage your billing</h4> <h4 class="dark:text-white lg:mt-0">{$t('settings.billing.manage')}</h4>
{#if $isFreePlan} {#if $isFreePlan}
You don't have any active billing {$t('settings.billing.active')}
{:else} {:else}
<p class="mb-3">We use Lemonsqueezy to help manage your billing</p> <p class="mb-3">{$t('settings.billing.lemonsqueezy')}</p>
<PrimaryButton <PrimaryButton
label="Open billing" label={$t('settings.billing.open_billing')}
variant={VARIANTS.CONTAINED_DARK} variant={VARIANTS.CONTAINED_DARK}
onClick={onOpenBilling} onClick={onOpenBilling}
isLoading={isRedirecting} isLoading={isRedirecting}
@@ -12,6 +12,7 @@
import SectionTitle from '../SectionTitle.svelte'; import SectionTitle from '../SectionTitle.svelte';
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte'; import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
import { updateOrgSiteNameValidation } from '$lib/utils/functions/validator'; import { updateOrgSiteNameValidation } from '$lib/utils/functions/validator';
import { t } from '$lib/utils/functions/translations';
type Error = { type Error = {
siteName: string; siteName: string;
@@ -45,7 +46,7 @@
console.log('Updating organisation', org); console.log('Updating organisation', org);
if (error) { if (error) {
console.log('Error: create organisation', error); console.log('Error: create organisation', error);
errors.siteName = 'Sitename already exists.'; errors.siteName = $t('add_org.sitename');
} else { } else {
snackbar.success(); snackbar.success();
$currentOrg.siteName = siteName; $currentOrg.siteName = siteName;
@@ -74,10 +75,12 @@
<Grid class="border rounded border-gray-200 dark:border-neutral-600 w-full mt-5"> <Grid class="border rounded border-gray-200 dark:border-neutral-600 w-full mt-5">
<Row class="py-7 border-bottom-c"> <Row class="py-7 border-bottom-c">
<Column sm={2} md={2} lg={4} class="text-lg"><SectionTitle>Add</SectionTitle></Column> <Column sm={2} md={2} lg={4} class="text-lg"
><SectionTitle>{$t('components.settings.domains.add')}</SectionTitle></Column
>
<Column sm={2} md={6} lg={8}> <Column sm={2} md={6} lg={8}>
<p class="text-md text-gray-500 dark:text-white mb-5"> <p class="text-md text-gray-500 dark:text-white mb-5">
Add your team mates or collaborators to your organization. Start working together {$t('components.settings.domains.work_together')}
</p> </p>
<div> <div>
@@ -94,7 +97,7 @@
/> />
<div class="flex items-center mb-6"> <div class="flex items-center mb-6">
<PrimaryButton <PrimaryButton
label="Update Domain" label={$t('components.settings.domains.update')}
className="py-4" className="py-4"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
onClick={handleChangeDomain} onClick={handleChangeDomain}
@@ -108,10 +111,12 @@
</Row> </Row>
<Row class="py-7 border-bottom-c"> <Row class="py-7 border-bottom-c">
<Column sm={2} md={2} lg={4} class="text-lg"><SectionTitle>Custom domain</SectionTitle></Column> <Column sm={2} md={2} lg={4} class="text-lg"
><SectionTitle>{$t('components.settings.domains.custom')}</SectionTitle></Column
>
<Column sm={2} md={6} lg={8}> <Column sm={2} md={6} lg={8}>
<div class="flex items-center"> <div class="flex items-center">
<p class="dark:text-white mr-3">Add your own domain name</p> <p class="dark:text-white mr-3">{$t('components.settings.domains.domain')}</p>
<ComingSoon /> <ComingSoon />
</div> </div>
</Column> </Column>
@@ -9,6 +9,7 @@
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import SectionTitle from '../SectionTitle.svelte'; import SectionTitle from '../SectionTitle.svelte';
import ArrowUpRightIcon from '$lib/components/Icons/ArrowTopRight.svelte'; import ArrowUpRightIcon from '$lib/components/Icons/ArrowTopRight.svelte';
import { t } from '$lib/utils/functions/translations';
let chatId: number | null; let chatId: number | null;
@@ -24,11 +25,11 @@
if (error) { if (error) {
console.log(error); console.log(error);
snackbar.error('Integration Failed, please try again later'); snackbar.error('snackbar.course_settings.error.failed_integration');
} else { } else {
$profile.telegram_chat_id = chatId; $profile.telegram_chat_id = chatId;
chatId = null; chatId = null;
snackbar.success('Integration successful'); snackbar.success('snackbar.course_settings.success.successful_integration');
} }
} }
@@ -39,11 +40,11 @@
.match({ id: $profile.id }); .match({ id: $profile.id });
if (error) { if (error) {
snackbar.error('Deletion Failed, please try again later'); snackbar.error('snackbar.course_settings.error.failed_deletion');
console.log(error); console.log(error);
} else { } else {
$profile.telegram_chat_id = null; $profile.telegram_chat_id = null;
snackbar.success('Integration deleted'); snackbar.success('snackbar.course_settings.success.successful_deletion');
} }
} }
</script> </script>
@@ -52,7 +53,7 @@
<Row class="flex lg:flex-row flex-col justify-center lg:justify-start py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col justify-center lg:justify-start py-7 border-bottom-c">
<Column sm={4} md={4} lg={4} class="flex items-center justify-center"> <Column sm={4} md={4} lg={4} class="flex items-center justify-center">
<img src="/telegram-svg.svg" alt="" class="w-10 mr-2" /> <img src="/telegram-svg.svg" alt="" class="w-10 mr-2" />
<SectionTitle>Telegram</SectionTitle></Column <SectionTitle>{$t('settings.integrations.heading')}</SectionTitle></Column
> >
<Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0 flex flex-col items-center lg:items-start"> <Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0 flex flex-col items-center lg:items-start">
@@ -60,25 +61,26 @@
<div class="flex flex-col items-center w-full"> <div class="flex flex-col items-center w-full">
<div class="flex items-center"> <div class="flex items-center">
<CheckmarkFilled size={32} class="mr-2" style="fill: green;" /> <CheckmarkFilled size={32} class="mr-2" style="fill: green;" />
<SectionTitle>Integration Successful</SectionTitle> <SectionTitle>{$t('settings.integrations.success_message')}</SectionTitle>
</div> </div>
<PrimaryButton className="mt-3" variant={VARIANTS.CONTAINED_DANGER} onClick={deleteChatId} <PrimaryButton className="mt-3" variant={VARIANTS.CONTAINED_DANGER} onClick={deleteChatId}
>Disconnect</PrimaryButton >{$t('settings.integrations.disconnect')}</PrimaryButton
> >
</div> </div>
{:else} {:else}
<div> <div>
<h3 class="font-normal text-lg"> <h3 class="font-normal text-lg">
Connect your account to telegram to get notified of any change within the application. {$t('settings.integrations.sub_heading')}
</h3> </h3>
<h4 class="font-normal">Step 1: Authenticate your account via the ClassroomIO bot.</h4> <h4 class="font-normal">{$t('settings.integrations.step_1')}</h4>
<PrimaryButton <PrimaryButton
className="mb-5" className="mb-5"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
onClick={() => window.open('https://t.me/classroomio_bot', '_blank')} onClick={() => window.open('https://t.me/classroomio_bot', '_blank')}
><span class="mr-2">Open Bot</span> <ArrowUpRightIcon /></PrimaryButton ><span class="mr-2">{$t('settings.integrations.button_1')}</span>
<ArrowUpRightIcon /></PrimaryButton
> >
<h4 class="font-normal">Step 2: Enter the Chat ID given to you by the bot.</h4> <h4 class="font-normal">{$t('settings.integrations.step_2')}</h4>
<TextField <TextField
bind:value={chatId} bind:value={chatId}
placeholder="360111" placeholder="360111"
@@ -86,7 +88,7 @@
type="number" type="number"
isRequired isRequired
/> />
<PrimaryButton onClick={addChatId}>Connect</PrimaryButton> <PrimaryButton onClick={addChatId}>{$t('settings.integrations.button_2')}</PrimaryButton>
</div> </div>
{/if} {/if}
</Column> </Column>
@@ -16,13 +16,14 @@
import type { OrgLandingPageJson } from './store'; import type { OrgLandingPageJson } from './store';
import UploadWidget from '$lib/components/UploadWidget/index.svelte'; import UploadWidget from '$lib/components/UploadWidget/index.svelte';
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store'; import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
import { t } from '$lib/utils/functions/translations';
let isSaving = false; let isSaving = false;
let creatingNewQuestion = false; let creatingNewQuestion = false;
let widgetKey = ''; let widgetKey = '';
const banner = [ const banner = [
{ value: 'video', label: 'Video' }, { value: 'video', label: `${$t('settings.landing_page.actions.banner_type.video')}` },
{ value: 'image', label: 'Image' } { value: 'image', label: `${$t('settings.landing_page.actions.banner_type.image')}` }
]; ];
const supabase = getSupabase(); const supabase = getSupabase();
@@ -91,8 +92,8 @@
.match({ id: $currentOrg.id }); .match({ id: $currentOrg.id });
if (error) { if (error) {
const message = error?.message || 'Please try again'; const message = error?.message || 'snackbar.lms.error.try_again';
snackbar.error(`Update failed: ${message}`); snackbar.error(`snackbar.lms.error.update ${message}`);
} else { } else {
$currentOrg.landingpage = $landingPageSettings; $currentOrg.landingpage = $landingPageSettings;
snackbar.success(); snackbar.success();
@@ -120,57 +121,61 @@
<Grid class="border-c rounded border-gray-200 dark:border-neutral-600 w-full mt-5 relative"> <Grid class="border-c rounded border-gray-200 dark:border-neutral-600 w-full mt-5 relative">
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}> <Column sm={4} md={4} lg={4}>
<SectionTitle>Landing Page</SectionTitle> <SectionTitle>{$t('settings.landing_page.heading')}</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.header.show} size="sm"> <Toggle bind:toggled={$landingPageSettings.header.show} size="sm">
<span slot="labelA" style="color: gray">Hide section</span> <span slot="labelA" style="color: gray">{$t('settings.landing_page.hide_section')}</span>
<span slot="labelB" style="color: gray">Show section</span> <span slot="labelB" style="color: gray">{$t('settings.landing_page.show_section')}</span>
</Toggle> </Toggle>
</Column> </Column>
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0"> <Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
<TextField <TextField
label="Title" label={$t('settings.landing_page.title')}
placeholder="Write your title here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
bind:value={$landingPageSettings.header.title} bind:value={$landingPageSettings.header.title}
className="w-full mb-5" className="w-full mb-5"
/> />
<TextField <TextField
label="Title-Highlight" label={$t('settings.landing_page.title_highlight')}
placeholder="Write your titlehighlight here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.highlight')}
className="w-full mb-5" className="w-full mb-5"
bind:value={$landingPageSettings.header.titleHighlight} bind:value={$landingPageSettings.header.titleHighlight}
/> />
<TextArea <TextArea
label="Subtitle" label={$t('settings.landing_page.subtitle')}
labelClassName="font-light" labelClassName="font-light"
placeholder="Write your subtitle here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_subtitle')}
className="w-full mb-5" className="w-full mb-5"
bind:value={$landingPageSettings.header.subtitle} bind:value={$landingPageSettings.header.subtitle}
isAIEnabled={true} isAIEnabled={true}
/> />
<SectionTitle>Actions</SectionTitle> <SectionTitle>{$t('settings.landing_page.actions.heading')}</SectionTitle>
<TextField <TextField
label="Label" label={$t('settings.landing_page.actions.label')}
placeholder="Write your label here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.label')}
className="w-full mt-3 mb-5" className="w-full mt-3 mb-5"
bind:value={$landingPageSettings.header.action.label} bind:value={$landingPageSettings.header.action.label}
/> />
<div class="gap-2 mb-5"> <div class="gap-2 mb-5">
<TextField <TextField
label="Link" label={$t('settings.landing_page.actions.link')}
placeholder="Write your link here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.link')}
bind:value={$landingPageSettings.header.action.link} bind:value={$landingPageSettings.header.action.link}
/> />
<Toggle bind:toggled={$landingPageSettings.header.action.redirect} size="sm"> <Toggle bind:toggled={$landingPageSettings.header.action.redirect} size="sm">
<span slot="labelA" style="color: gray">No redirect</span> <span slot="labelA" style="color: gray"
<span slot="labelB" style="color: gray">Redirect</span> >{$t('settings.landing_page.actions.no_redirect')}</span
>
<span slot="labelB" style="color: gray"
>{$t('settings.landing_page.actions.redirect')}</span
>
</Toggle> </Toggle>
</div> </div>
<div /> <div />
<RadioButtonGroup <RadioButtonGroup
legendText="Banner Type" legendText={$t('settings.landing_page.actions.banner_type.heading')}
bind:selected={$landingPageSettings.header.banner.type} bind:selected={$landingPageSettings.header.banner.type}
class="mt-10 mb-5" class="mt-10 mb-5"
> >
@@ -181,15 +186,15 @@
{#if $landingPageSettings.header.banner.type === 'video'} {#if $landingPageSettings.header.banner.type === 'video'}
<div class="gap-2 mt-3 mb-5"> <div class="gap-2 mt-3 mb-5">
<TextField <TextField
label="Link" label={$t('settings.landing_page.actions.link')}
placeholder="Write your video link here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.video')}
bind:value={$landingPageSettings.header.banner.video} bind:value={$landingPageSettings.header.banner.video}
/> />
</div> </div>
{:else} {:else}
<PrimaryButton <PrimaryButton
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="Select Image" label={$t('settings.landing_page.about.select_image')}
className="mt-3" className="mt-3"
onClick={() => widgetControl('banner')} onClick={() => widgetControl('banner')}
/> />
@@ -203,8 +208,12 @@
{/if} {/if}
<Toggle bind:toggled={$landingPageSettings.header.banner.show} size="sm"> <Toggle bind:toggled={$landingPageSettings.header.banner.show} size="sm">
<span slot="labelA" style="color: gray">Hide banner</span> <span slot="labelA" style="color: gray"
<span slot="labelB" style="color: gray">Show banner</span> >{$t('settings.landing_page.actions.hide_banner')}</span
>
<span slot="labelB" style="color: gray"
>{$t('settings.landing_page.actions.show_banner')}</span
>
</Toggle> </Toggle>
{#if $handleOpenWidget.open && widgetKey === 'banner'} {#if $handleOpenWidget.open && widgetKey === 'banner'}
<UploadWidget bind:imageURL={$landingPageSettings.header.banner.image} /> <UploadWidget bind:imageURL={$landingPageSettings.header.banner.image} />
@@ -214,34 +223,34 @@
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4} <Column sm={4} md={4} lg={4}
><SectionTitle>About</SectionTitle> ><SectionTitle>{$t('settings.landing_page.about.heading')}</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.aboutUs.show} size="sm"> <Toggle bind:toggled={$landingPageSettings.aboutUs.show} size="sm">
<span slot="labelA" style="color: gray">Hide section</span> <span slot="labelA" style="color: gray">{$t('settings.landing_page.hide_section')}</span>
<span slot="labelB" style="color: gray">Show section</span> <span slot="labelB" style="color: gray">{$t('settings.landing_page.show_section')}</span>
</Toggle></Column </Toggle></Column
> >
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0"> <Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
<TextField <TextField
label="Title" label={$t('settings.landing_page.about.title')}
placeholder="Write your title here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
bind:value={$landingPageSettings.aboutUs.title} bind:value={$landingPageSettings.aboutUs.title}
className="mb-5" className="mb-5"
/> />
<TextArea <TextArea
label="Content" label={$t('settings.landing_page.about.content')}
labelClassName="font-light" labelClassName="font-light"
placeholder="Write your Content here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.content')}
className="mb-5" className="mb-5"
bind:value={$landingPageSettings.aboutUs.content} bind:value={$landingPageSettings.aboutUs.content}
isAIEnabled={true} isAIEnabled={true}
/> />
<div> <div>
<p class="font-bold">Upload an image</p> <p class="font-bold">{$t('settings.landing_page.about.upload_an_image')}</p>
<PrimaryButton <PrimaryButton
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="Select Image" label={$t('settings.landing_page.about.select_image')}
className="mt-3" className="mt-3"
onClick={() => widgetControl('about-us')} onClick={() => widgetControl('about-us')}
/> />
@@ -261,27 +270,31 @@
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4} <Column sm={4} md={4} lg={4}
><SectionTitle>Courses</SectionTitle> ><SectionTitle>{$t('settings.landing_page.courses.heading')}</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.courses.show} size="sm"> <Toggle bind:toggled={$landingPageSettings.courses.show} size="sm">
<span slot="labelA" style="color: gray">Hide section</span> <span slot="labelA" style="color: gray"
<span slot="labelB" style="color: gray">Show section</span> >{$t('settings.landing_page.courses.hide_section')}</span
>
<span slot="labelB" style="color: gray"
>{$t('settings.landing_page.courses.show_section')}</span
>
</Toggle> </Toggle>
</Column> </Column>
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0"> <Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
<TextField <TextField
label="Title" label={$t('settings.landing_page.courses.title')}
placeholder="Write your title here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
bind:value={$landingPageSettings.courses.title} bind:value={$landingPageSettings.courses.title}
/> />
<TextField <TextField
label="Title-Highlight" label={$t('settings.landing_page.courses.title_highlight')}
placeholder="Write your title highlight here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.highlight')}
bind:value={$landingPageSettings.courses.titleHighlight} bind:value={$landingPageSettings.courses.titleHighlight}
/> />
<TextArea <TextArea
label="Subtitle" label={$t('settings.landing_page.courses.subtitle')}
labelClassName="font-light" labelClassName="font-light"
placeholder="Write your subtitle here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_subtitle')}
bind:value={$landingPageSettings.courses.subtitle} bind:value={$landingPageSettings.courses.subtitle}
isAIEnabled={true} isAIEnabled={true}
/> />
@@ -290,31 +303,37 @@
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4} <Column sm={4} md={4} lg={4}
><SectionTitle>Frequently Asked Questions</SectionTitle> ><SectionTitle>{$t('settings.landing_page.faq.heading')}</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.faq.show} size="sm"> <Toggle bind:toggled={$landingPageSettings.faq.show} size="sm">
<span slot="labelA" style="color: gray">Hide section</span> <span slot="labelA" style="color: gray">{$t('settings.landing_page.faq.hide_section')}</span
<span slot="labelB" style="color: gray">Show section</span> >
<span slot="labelB" style="color: gray">{$t('settings.landing_page.faq.show_section')}</span
>
</Toggle> </Toggle>
</Column> </Column>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<TextField <TextField
label="Title" label={$t('settings.landing_page.faq.title')}
placeholder="Write your title here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
bind:value={$landingPageSettings.faq.title} bind:value={$landingPageSettings.faq.title}
className="mb-5" className="mb-5"
/> />
{#each $landingPageSettings.faq.questions as item (item.id)} {#each $landingPageSettings.faq.questions as item (item.id)}
<div class="mb-3"> <div class="mb-3">
<TextField <TextField
label="Question" label={$t('settings.landing_page.faq.question')}
placeholder="Write your question here" placeholder={$t(
'course.navItem.lessons.exercises.all_exercises.write_your_question_here'
)}
bind:value={item.title} bind:value={item.title}
className="mb-5" className="mb-5"
/> />
<TextArea <TextArea
label="Answer" label={$t('settings.landing_page.faq.answer')}
labelClassName="font-light" labelClassName="font-light"
placeholder="Write your answer here" placeholder={$t(
'course.navItem.lessons.exercises.all_exercises.write_your_answer_here'
)}
bind:value={item.content} bind:value={item.content}
className="mb-5" className="mb-5"
isAIEnabled={true} isAIEnabled={true}
@@ -326,68 +345,86 @@
{/each} {/each}
{#if creatingNewQuestion} {#if creatingNewQuestion}
<TextField <TextField
label="Question" label={$t('settings.landing_page.faq.question')}
placeholder="Write your question here" placeholder={$t(
'course.navItem.lessons.exercises.all_exercises.write_your_question_here'
)}
bind:value={newQuestion.title} bind:value={newQuestion.title}
className="mb-5" className="mb-5"
/> />
<TextArea <TextArea
label="Answer" label={$t('settings.landing_page.faq.answer')}
labelClassName="font-light" labelClassName="font-light"
placeholder="Write your answer here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_answer_here')}
bind:value={newQuestion.content} bind:value={newQuestion.content}
className="mb-5" className="mb-5"
isAIEnabled={true} isAIEnabled={true}
/> />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<PrimaryButton variant={VARIANTS.OUTLINED} label="Save" onClick={saveNewFAQ} /> <PrimaryButton
<PrimaryButton variant={VARIANTS.OUTLINED} label="Cancel" onClick={cancelNewFAQ} /> variant={VARIANTS.OUTLINED}
label={$t('settings.landing_page.faq.save')}
onClick={saveNewFAQ}
/>
<PrimaryButton
variant={VARIANTS.OUTLINED}
label={$t('settings.landing_page.faq.cancel')}
onClick={cancelNewFAQ}
/>
</div> </div>
{:else} {:else}
<PrimaryButton variant={VARIANTS.OUTLINED} label="Add New Faq" onClick={createNewFaq} /> <PrimaryButton
variant={VARIANTS.OUTLINED}
label={$t('settings.landing_page.faq.button')}
onClick={createNewFaq}
/>
{/if} {/if}
</Column> </Column>
</Row> </Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4} <Column sm={4} md={4} lg={4}
><SectionTitle>Contact us</SectionTitle> ><SectionTitle>{$t('settings.landing_page.contact_us.heading')}</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.contact.show} size="sm"> <Toggle bind:toggled={$landingPageSettings.contact.show} size="sm">
<span slot="labelA" style="color: gray">Hide section</span> <span slot="labelA" style="color: gray"
<span slot="labelB" style="color: gray">Show section</span> >{$t('settings.landing_page.contact_us.hide_section')}</span
>
<span slot="labelB" style="color: gray"
>{$t('settings.landing_page.contact_us.show_section')}</span
>
</Toggle> </Toggle>
</Column> </Column>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<TextField <TextField
label="Title" label={$t('settings.landing_page.contact_us.title')}
placeholder="Write your title here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
bind:value={$landingPageSettings.contact.title} bind:value={$landingPageSettings.contact.title}
className="mb-5" className="mb-5"
/> />
<TextField <TextField
label="Title-highlight" label={$t('settings.landing_page.contact_us.title_highlight')}
labelClassName="font-light" labelClassName="font-light"
placeholder="Write your title highlight here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.highlight')}
className="mb-5" className="mb-5"
bind:value={$landingPageSettings.contact.titleHighlight} bind:value={$landingPageSettings.contact.titleHighlight}
/> />
<TextArea <TextArea
label="Subtitle" label={$t('settings.landing_page.contact_us.subtitle')}
labelClassName="font-light" labelClassName="font-light"
placeholder="Write your subtitle here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_subtitle')}
className="mt-3 mb-5" className="mt-3 mb-5"
bind:value={$landingPageSettings.contact.subtitle} bind:value={$landingPageSettings.contact.subtitle}
isAIEnabled={true} isAIEnabled={true}
/> />
<TextField <TextField
label="Phone number" label={$t('settings.landing_page.contact_us.phone_number')}
placeholder="Write your phone number here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.number')}
className="mt-3 mb-5" className="mt-3 mb-5"
bind:value={$landingPageSettings.contact.phone} bind:value={$landingPageSettings.contact.phone}
/> />
<TextField <TextField
label="Email" label={$t('settings.landing_page.contact_us.email')}
placeholder="Write your email here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.email')}
className="mt-3 mb-5" className="mt-3 mb-5"
bind:value={$landingPageSettings.contact.email} bind:value={$landingPageSettings.contact.email}
/> />
@@ -396,31 +433,35 @@
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4} <Column sm={4} md={4} lg={4}
><SectionTitle>Mailing List</SectionTitle> ><SectionTitle>{$t('settings.landing_page.mailing_list.heading')}</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.mailinglist.show} size="sm"> <Toggle bind:toggled={$landingPageSettings.mailinglist.show} size="sm">
<span slot="labelA" style="color: gray">Hide section</span> <span slot="labelA" style="color: gray"
<span slot="labelB" style="color: gray">Show section</span> >{$t('settings.landing_page.mailing_list.hide_section')}</span
>
<span slot="labelB" style="color: gray"
>{$t('settings.landing_page.mailing_list.show_section')}</span
>
</Toggle> </Toggle>
</Column> </Column>
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0"> <Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
<TextField <TextField
label="Title" label={$t('settings.landing_page.mailing_list.title')}
placeholder="Write your title here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
bind:value={$landingPageSettings.mailinglist.title} bind:value={$landingPageSettings.mailinglist.title}
className="mb-5" className="mb-5"
/> />
<TextArea <TextArea
label="Subtitle" label={$t('settings.landing_page.mailing_list.subtitle')}
labelClassName="font-light" labelClassName="font-light"
placeholder="Write your subtitle here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_subtitle')}
className="mb-5" className="mb-5"
bind:value={$landingPageSettings.mailinglist.subtitle} bind:value={$landingPageSettings.mailinglist.subtitle}
isAIEnabled={true} isAIEnabled={true}
/> />
<TextField <TextField
label="Button-label" label={$t('settings.landing_page.mailing_list.button_label')}
placeholder="Write your label here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.label')}
className="mb-5" className="mb-5"
bind:value={$landingPageSettings.mailinglist.buttonLabel} bind:value={$landingPageSettings.mailinglist.buttonLabel}
/> />
@@ -429,16 +470,20 @@
<Row class="flex lg:flex-row flex-col py-7"> <Row class="flex lg:flex-row flex-col py-7">
<Column sm={4} md={4} lg={4} <Column sm={4} md={4} lg={4}
><SectionTitle>Footer</SectionTitle> ><SectionTitle>{$t('settings.landing_page.footer.heading')}</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.footer.show} size="sm"> <Toggle bind:toggled={$landingPageSettings.footer.show} size="sm">
<span slot="labelA" style="color: gray">Hide section</span> <span slot="labelA" style="color: gray"
<span slot="labelB" style="color: gray">Show section</span> >{$t('settings.landing_page.footer.hide_section')}</span
>
<span slot="labelB" style="color: gray"
>{$t('settings.landing_page.footer.show_section')}</span
>
</Toggle> </Toggle>
</Column> </Column>
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0"> <Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
<TextField <TextField
label="Facebook" label={$t('settings.landing_page.footer.facebook')}
placeholder="Write your Facebook link here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.facebook')}
bind:value={$landingPageSettings.footer.facebook} bind:value={$landingPageSettings.footer.facebook}
className="mb-5" className="mb-5"
/> />
@@ -449,14 +494,14 @@
bind:value={$landingPageSettings.footer.instagram} bind:value={$landingPageSettings.footer.instagram}
/> --> /> -->
<TextField <TextField
label="Twitter" label={$t('settings.landing_page.footer.twitter')}
placeholder="Write your Twitter link here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.twitter')}
className="mb-5" className="mb-5"
bind:value={$landingPageSettings.footer.twitter} bind:value={$landingPageSettings.footer.twitter}
/> />
<TextField <TextField
label="Linkedin" label={$t('settings.landing_page.footer.linkedin')}
placeholder="Write your Linkedin link here" placeholder={$t('course.navItem.lessons.exercises.all_exercises.linkedin')}
className="mb-5" className="mb-5"
bind:value={$landingPageSettings.footer.linkedin} bind:value={$landingPageSettings.footer.linkedin}
/> />
@@ -464,7 +509,7 @@
</Row> </Row>
<div class="sticky desktop float-right bottom-12 mr-2 z-[120]"> <div class="sticky desktop float-right bottom-12 mr-2 z-[120]">
<PrimaryButton <PrimaryButton
label="Save Changes" label={$t('settings.landing_page.save_changes')}
isLoading={isSaving} isLoading={isSaving}
isDisabled={isSaving} isDisabled={isSaving}
onClick={handleSave} onClick={handleSave}
@@ -0,0 +1,28 @@
<script>
import { Dropdown } from 'carbon-components-svelte';
import { t, initialized } from '$lib/utils/functions/translations';
import { LANGUAGES } from '$lib/utils/constants/translation';
import { LOCALE } from '$lib/utils/types';
export let className = '';
export let value = LOCALE.EN;
export let hasLangChanged = false;
const dropdownItems = [{ id: '', text: 'Pick a Language' }, ...LANGUAGES];
function handleSelect(event) {
value = event.detail.selectedId;
hasLangChanged = true;
}
</script>
{#if $initialized}
<Dropdown
titleText={$t('content.toggle_label')}
items={dropdownItems}
selectedId={value}
on:select={handleSelect}
class="h-full {className}"
/>
{/if}
@@ -10,6 +10,7 @@
import { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org'; import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
import SectionTitle from '../SectionTitle.svelte'; import SectionTitle from '../SectionTitle.svelte';
import { t } from '$lib/utils/functions/translations';
import { isFreePlan } from '$lib/utils/store/org'; import { isFreePlan } from '$lib/utils/store/org';
let avatar; let avatar;
@@ -75,16 +76,16 @@
...updates ...updates
})); }));
snackbar.success('Update successful'); snackbar.success('snackbar.course_settings.success.update_successful');
if (error) throw error; if (error) throw error;
} catch (error) { } catch (error) {
let message = error.message; let message = error.message;
if (message.includes('profile_username_key')) { if (message.includes('profile_username_key')) {
message = 'username already exists'; message = $t('snackbar.lms.error.username_exists');
} }
snackbar.error(`Update failed: ${message}`); snackbar.error(`${$t('snackbar.lms.error.update')} ${message}`);
loading = false; loading = false;
} finally { } finally {
loading = false; loading = false;
@@ -98,10 +99,13 @@
<Grid class="border-c rounded border-gray-200 dark:border-neutral-600 w-full mt-5"> <Grid class="border-c rounded border-gray-200 dark:border-neutral-600 w-full mt-5">
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}><SectionTitle>Organization Profile</SectionTitle></Column> <Column sm={4} md={4} lg={4}
><SectionTitle>{$t('settings.organization.organization_profile.heading')}</SectionTitle
></Column
>
<Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0 flex flex-col items-center lg:items-start"> <Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0 flex flex-col items-center lg:items-start">
<TextField <TextField
label="Organization Name" label={$t('settings.organization.organization_profile.organization_name')}
bind:value={$currentOrg.name} bind:value={$currentOrg.name}
className="w-full lg:w-60 mb-5" className="w-full lg:w-60 mb-5"
/> />
@@ -112,7 +116,7 @@
widthHeight="w-24 h-24" widthHeight="w-24 h-24"
/> />
<PrimaryButton <PrimaryButton
label="Update Organization" label={$t('settings.organization.organization_profile.update_organization')}
className="px-6 py-3 lg:mr-5 mt-5" className="px-6 py-3 lg:mr-5 mt-5"
isLoading={loading} isLoading={loading}
isDisabled={loading} isDisabled={loading}
@@ -121,9 +125,14 @@
</Column> </Column>
</Row> </Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}><SectionTitle>Theme</SectionTitle></Column> <Column sm={4} md={4} lg={4}
><SectionTitle>{$t('settings.organization.organization_profile.theme.heading')}</SectionTitle
></Column
>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<h4 class="dark:text-white lg:mt-0">Set your brand color</h4> <h4 class="dark:text-white lg:mt-0">
{$t('settings.organization.organization_profile.theme.sub_heading')}
</h4>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
@@ -169,14 +178,20 @@
</Column> </Column>
</Row> </Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}><SectionTitle>Custom Domain</SectionTitle></Column> <Column sm={4} md={4} lg={4}
><SectionTitle
>{$t('settings.organization.organization_profile.custom_domain.heading')}</SectionTitle
></Column
>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<h4 class="dark:text-white lg:mt-0">Customise your Domain</h4> <h4 class="dark:text-white lg:mt-0">
{$t('settings.organization.organization_profile.custom_domain.sub_heading')}
</h4>
<p class="text-sm text-gray-500 dark:text-white"> <p class="text-sm text-gray-500 dark:text-white">
Create a custom URL so your audience can get to your organization easily {$t('settings.organization.organization_profile.custom_domain.body')}
</p> </p>
<PrimaryButton <PrimaryButton
label="Edit domain" label={$t('settings.organization.organization_profile.custom_domain.button')}
className="my-7 py-5 px-10" className="my-7 py-5 px-10"
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
onClick={() => gotoSetting('/domains')} onClick={() => gotoSetting('/domains')}
@@ -184,11 +199,16 @@
</Column> </Column>
</Row> </Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c"> <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}><SectionTitle>Team</SectionTitle></Column> <Column sm={4} md={4} lg={4}
><SectionTitle>{$t('settings.organization.organization_profile.team.heading')}</SectionTitle
></Column
>
<Column sm={8} md={8} lg={8}> <Column sm={8} md={8} lg={8}>
<h4 class="dark:text-white lg:mt-0">Set up your website</h4> <h4 class="dark:text-white lg:mt-0">
{$t('settings.organization.organization_profile.team.sub_heading')}
</h4>
<p class="text-sm text-gray-500 dark:text-white"> <p class="text-sm text-gray-500 dark:text-white">
Add team mates to your organization so you can both collaborate on projects. {$t('settings.organization.organization_profile.team.body')}
</p> </p>
<PrimaryButton <PrimaryButton
className="my-7 py-5 px-10 flex items-center gap-2 justify-center" className="my-7 py-5 px-10 flex items-center gap-2 justify-center"
@@ -10,18 +10,25 @@
import { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
import LogoutButton from '$lib/components/Buttons/Logout/index.svelte'; import LogoutButton from '$lib/components/Buttons/Logout/index.svelte';
import generateUUID from '$lib/utils/functions/generateUUID'; import generateUUID from '$lib/utils/functions/generateUUID';
import { t } from '$lib/utils/functions/translations';
import LanguagePicker from './LanguagePicker.svelte';
import { handleLocaleChange } from '$lib/utils/functions/translations';
let avatar = ''; let avatar = '';
let loading = false; let loading = false;
let hasLangChanged = false;
let locale = '';
async function handleUpdate() { async function handleUpdate() {
try { try {
console.log({ hasLangChanged });
loading = true; loading = true;
const updates = { const updates = {
fullname: $profile.fullname, fullname: $profile.fullname,
username: $profile.username, username: $profile.username,
email: $profile.email email: $profile.email,
locale
}; };
if (avatar) { if (avatar) {
@@ -46,26 +53,32 @@
..._profile, ..._profile,
...updates ...updates
})); }));
snackbar.success('Update successful'); snackbar.success('snackbar.course_settings.success.update_successful');
if (hasLangChanged) {
handleLocaleChange(locale);
}
if (error) throw error; if (error) throw error;
} catch (error) { } catch (error) {
let message = error.message; let message = error.message;
if (message.includes('profile_username_key')) { if (message.includes('profile_username_key')) {
message = 'username already exists'; message = $t('snackbar.lms.error.username_exists');
} }
snackbar.success(`Update failed: ${message}`); snackbar.error(`${$t('snackbar.lms.error.update')} ${message}`);
loading = false; loading = false;
} finally { } finally {
loading = false; loading = false;
} }
} }
$: locale = !locale ? $profile.locale : locale;
</script> </script>
<Grid class="border-c rounded border-gray-200 dark:border-neutral-600 w-full mt-5"> <Grid class="border-c rounded border-gray-200 dark:border-neutral-600 w-full mt-5">
<Row class="flex flex-col lg:flex-row items-center lg:items-start py-7 border-bottom-c "> <Row class="flex flex-col lg:flex-row items-center lg:items-start py-7 border-bottom-c ">
<Column sm={4} md={8} lg={4} class="mt-2 md:mt-0"> <Column sm={4} md={8} lg={4} class="mt-2 md:mt-0">
<SectionTitle>Profile Picture</SectionTitle> <SectionTitle>{$t('settings.profile.profile_picture.heading')}</SectionTitle>
</Column> </Column>
<Column sm={2} md={4} lg={8} class="mt-2 lg:mt-0"> <Column sm={2} md={4} lg={8} class="mt-2 lg:mt-0">
<UploadImage bind:avatar src={$profile.avatar_url} widthHeight="w-16 h-16 lg:w-24 lg:h-24" /> <UploadImage bind:avatar src={$profile.avatar_url} widthHeight="w-16 h-16 lg:w-24 lg:h-24" />
@@ -73,17 +86,31 @@
</Row> </Row>
<Row class="flex flex-col lg:flex-row py-7 border-bottom-c"> <Row class="flex flex-col lg:flex-row py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}> <Column sm={4} md={4} lg={4}>
<SectionTitle>Personal Information</SectionTitle> <SectionTitle>{$t('settings.profile.personal_information.heading')}</SectionTitle>
</Column> </Column>
<Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0"> <Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0">
<TextField label="Full Name" bind:value={$profile.fullname} className="w-full lg:w-60 mb-4" /> <TextField
<TextField label="Username" bind:value={$profile.username} className="w-full lg:w-60 mb-4" /> label={$t('settings.profile.personal_information.full_name')}
<TextField label="Email" bind:value={$profile.email} className="w-full lg:w-60 mb-4" /> bind:value={$profile.fullname}
className="w-full lg:w-60 mb-4"
/>
<TextField
label={$t('settings.profile.personal_information.username')}
bind:value={$profile.username}
className="w-full lg:w-60 mb-4"
/>
<TextField
label={$t('settings.profile.personal_information.email')}
bind:value={$profile.email}
className="w-full lg:w-60 mb-4"
/>
<LanguagePicker bind:hasLangChanged bind:value={locale} className="w-full lg:w-60 mb-4" />
</Column> </Column>
</Row> </Row>
<Row class="m-5 w-full flex items-center gap-2 lg:justify-center"> <Row class="m-5 w-full flex items-center gap-2 lg:justify-center">
<PrimaryButton <PrimaryButton
label="Update Profile" label={$t('settings.profile.update_profile')}
variant={VARIANTS.CONTAINED_DARK} variant={VARIANTS.CONTAINED_DARK}
className="mr-5" className="mr-5"
isLoading={loading} isLoading={loading}
@@ -46,7 +46,7 @@
); );
if (doesEmailExist) { if (doesEmailExist) {
snackbar.error('User Already Exists'); snackbar.error('snackbar.team_members.user_exists');
isLoading = false; isLoading = false;
return; return;
} }
@@ -65,7 +65,7 @@
apiError = `${error}`; apiError = `${error}`;
console.error('onSendInvite:', error); console.error('onSendInvite:', error);
snackbar.error(`Failed to send invite, please try again`); snackbar.error(`snackbar.team_members.invite_fail`);
isLoading = false; isLoading = false;
return; return;
} }
@@ -95,7 +95,7 @@
const isLast = index === emails.length - 1; const isLast = index === emails.length - 1;
if (isLast) { if (isLast) {
snackbar.success('Invite Sent!'); snackbar.success('snackbar.team_members.invite_sent');
emailsStr = ''; emailsStr = '';
isLoading = false; isLoading = false;
@@ -110,7 +110,7 @@
if (error) { if (error) {
console.error('onRemove:', error); console.error('onRemove:', error);
snackbar.error('Failed to remove user, please try again'); snackbar.error('snackbar.team_members.remove_fail');
} else { } else {
orgTeam.update((team) => [...team.filter((member) => member.id !== id)]); orgTeam.update((team) => [...team.filter((member) => member.id !== id)]);
} }
@@ -11,6 +11,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { isOrgAdmin } from '$lib/utils/store/org'; import { isOrgAdmin } from '$lib/utils/store/org';
import Integrations from './Integrations.svelte'; import Integrations from './Integrations.svelte';
import { t } from '$lib/utils/functions/translations';
let selected = 0; let selected = 0;
let query = new URLSearchParams($page.url.search); let query = new URLSearchParams($page.url.search);
@@ -44,35 +45,35 @@
tabs = [ tabs = [
{ {
key: 0, key: 0,
label: 'Profile', label: $t('settings.tabs.1'),
tabKey: '', tabKey: '',
href: $page.url.pathname, href: $page.url.pathname,
disabled: false disabled: false
}, },
{ {
key: 1, key: 1,
label: 'Organization', label: $t('settings.tabs.2'),
tabKey: 'org', tabKey: 'org',
href: `${$page.url.pathname}?tab=org`, href: `${$page.url.pathname}?tab=org`,
disabled: !$isOrgAdmin disabled: !$isOrgAdmin
}, },
{ {
key: 2, key: 2,
label: 'LandingPage', label: $t('settings.tabs.3'),
tabKey: 'landingpage', tabKey: 'landingpage',
href: `${$page.url.pathname}?tab=landingpage`, href: `${$page.url.pathname}?tab=landingpage`,
disabled: !$isOrgAdmin disabled: !$isOrgAdmin
}, },
{ {
key: 3, key: 3,
label: 'Billing', label: $t('settings.tabs.4'),
tabKey: 'billing', tabKey: 'billing',
href: `${$page.url.pathname}?tab=billing`, href: `${$page.url.pathname}?tab=billing`,
disabled: !$isOrgAdmin disabled: !$isOrgAdmin
}, },
{ {
key: 4, key: 4,
label: 'Integrations', label: $t('settings.tabs.5'),
tabKey: 'integrations', tabKey: 'integrations',
href: `${$page.url.pathname}?tab=integrations`, href: `${$page.url.pathname}?tab=integrations`,
disabled: false disabled: false
@@ -16,13 +16,16 @@
import { profile } from '$lib/utils/store/user'; import { profile } from '$lib/utils/store/user';
import { NavClasses } from '$lib/utils/constants/reusableClass'; import { NavClasses } from '$lib/utils/constants/reusableClass';
import { sideBar } from './store'; import { sideBar } from './store';
import { t } from '$lib/utils/functions/translations';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
let menuItems: { interface menuItems {
path: string;
label: string; label: string;
path: string;
show: boolean; show: boolean;
}[] = []; }
let menuItems: menuItems[] = [];
function isActive(pagePath: string, itemPath: string) { function isActive(pagePath: string, itemPath: string) {
const pageLinkItems = pagePath.split('/'); const pageLinkItems = pagePath.split('/');
@@ -42,43 +45,70 @@
goto(window.location.pathname + '?upgrade=true'); goto(window.location.pathname + '?upgrade=true');
}; };
$: { // $: {
menuItems = [ // menuItems = [
{ // {
path: '', // path: '',
label: 'Dashboard', // label: 'Dashboard',
show: true // show: true
}, // },
// { // // {
// path: '/quiz', // // path: '/quiz',
// label: 'Quizzes' // // label: 'Quizzes'
// }, // // },
{ // {
path: '/courses', // path: '/courses',
label: 'Courses', // label: 'Courses',
show: true // show: true
}, // },
{ // {
path: '/community', // path: '/community',
label: 'Community', // label: 'Community',
show: true // show: true
}, // },
// { // // {
// path: '/site', // // path: '/site',
// label: 'Site settings', // // label: 'Site settings',
// }, // // },
{ // {
path: '/audience', // path: '/audience',
label: 'Audience', // label: 'Audience',
show: true // show: true
}, // },
{ // {
path: '/setup', // path: '/setup',
label: 'Setup', // label: 'Setup',
show: $isOrgAdmin // show: $isOrgAdmin
} // }
]; // ];
} // }
$: menuItems = [
{
path: '',
label: $t('org_navigation.dashboard'),
show: true
},
{
path: '/courses',
label: $t('org_navigation.courses'),
show: true
},
{
path: '/community',
label: $t('org_navigation.community'),
show: true
},
{
path: '/audience',
label: $t('org_navigation.audience'),
show: true
},
{
path: '/setup',
label: $t('org_navigation.setup'),
show: $isOrgAdmin
}
];
</script> </script>
<aside <aside
@@ -138,10 +168,14 @@
> >
<img src="/upgrade.png" alt="upgrade" class="h-16 w-16" /> <img src="/upgrade.png" alt="upgrade" class="h-16 w-16" />
<span class="flex flex-col gap-1"> <span class="flex flex-col gap-1">
<p class="text-base font-semibold">Become an Early Adopter</p> <p class="text-base font-semibold">{$t('org_navigation.early_adopter')}</p>
<p class="text-xs">Unlock unlimited features and invest in our future</p> <p class="text-xs">{$t('org_navigation.unlock')}</p>
</span> </span>
<PrimaryButton label="Upgrade Now" onClick={openModal} className="font-normal" /> <PrimaryButton
label={$t('org_navigation.upgrade')}
onClick={openModal}
className="font-normal"
/>
</div> </div>
{/if} {/if}
@@ -149,7 +183,7 @@
<a href={$currentOrgPath} class="text-black no-underline" on:click={toggleSidebar}> <a href={$currentOrgPath} class="text-black no-underline" on:click={toggleSidebar}>
<li class="mb-2 flex items-center rounded px-2.5 py-1.5"> <li class="mb-2 flex items-center rounded px-2.5 py-1.5">
<HelpIcon size={20} class="carbon-icon dark:text-white" /> <HelpIcon size={20} class="carbon-icon dark:text-white" />
<p class="ml-2.5 dark:text-white text-sm font-medium">Help</p> <p class="ml-2.5 dark:text-white text-sm font-medium">{$t('org_navigation.help')}</p>
</li> </li>
</a> </a>
<a href="{$currentOrgPath}/settings" class="text-black no-underline" on:click={toggleSidebar}> <a href="{$currentOrgPath}/settings" class="text-black no-underline" on:click={toggleSidebar}>
@@ -167,7 +201,7 @@
width="w-[1.2rem]" width="w-[1.2rem]"
height="h-[1.2rem]" height="h-[1.2rem]"
/> />
<p class="ml-2.5 text-sm font-medium">Settings</p> <p class="ml-2.5 text-sm font-medium">{$t('org_navigation.settings')}</p>
</li> </li>
</a> </a>
</ul> </ul>
@@ -8,6 +8,7 @@
NOTIFICATION_NAME NOTIFICATION_NAME
} 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';
const WAIT_SEC = 120; const WAIT_SEC = 120;
const WAIT_TIME = WAIT_SEC * 1000; const WAIT_TIME = WAIT_SEC * 1000;
@@ -31,7 +32,7 @@
isSent = true; isSent = true;
} catch (error) { } catch (error) {
snackbar.error('error while sending code'); snackbar.error('verify_email_modal.snackbar.error');
} finally { } finally {
loading = false; loading = false;
} }
@@ -53,9 +54,11 @@
<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">Verify your Email</p> <p class="font-bold text-xl">{$t('verify_email_modal.heading')}</p>
<p class="text-sm w-[70%] text-gray-500"> <p class="text-sm w-[70%] text-gray-700">
We've sent a verification email to {$profile.email} to confirm the validity of the email provided. {$t('verify_email_modal.sent_verification')}
{$profile.email}
{$t('verify_email_modal.to_confirm')}
</p> </p>
<div class="flex items-center flex-col"> <div class="flex items-center flex-col">
@@ -65,13 +68,17 @@
onClick={resendVerificationCode} onClick={resendVerificationCode}
> >
{#if loading} {#if loading}
Loading... {$t('verify_email_modal.loading')}
{:else} {:else}
Resend Verification Code {$t('verify_email_modal.resend')}
{/if} {/if}
</PrimaryButton> </PrimaryButton>
{#if isSent} {#if isSent}
<p class="text-xs text-gray-700">Resend verification in {countDown} seconds</p> <p class="text-xs text-gray-700">
{$t('verify_email_modal.resend_in')}
{countDown}
{$t('verify_email_modal.seconds')}
</p>
{/if} {/if}
</div> </div>
</div> </div>
@@ -2,6 +2,7 @@
import NewButton from '../PrimaryContainedButton/index.svelte'; import NewButton from '../PrimaryContainedButton/index.svelte';
import TextField from '../Form/TextField.svelte'; import TextField from '../Form/TextField.svelte';
import Chip from '../Chip/index.svelte'; import Chip from '../Chip/index.svelte';
import { t } from '$lib/utils/functions/translations';
export let title = ''; export let title = '';
export let overidableStyle = ''; export let overidableStyle = '';
@@ -43,7 +44,7 @@
{:else} {:else}
<TextField <TextField
bind:value={title} bind:value={title}
placeholder="Course title" placeholder={$t('course.navItem.settings.course_title')}
onChange={() => { onChange={() => {
enterEditTitleMode = false; enterEditTitleMode = false;
@@ -5,6 +5,7 @@
import Checkbox from '$lib/components/Form/Checkbox.svelte'; import Checkbox from '$lib/components/Form/Checkbox.svelte';
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte'; import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
import Grade from '$lib/components/Question/Grade.svelte'; import Grade from '$lib/components/Question/Grade.svelte';
import { t } from '$lib/utils/functions/translations';
export let title = ''; export let title = '';
export let index = 1; export let index = 1;
@@ -102,7 +103,7 @@
<div class="mt-3 flex items-center justify-between w-full"> <div class="mt-3 flex items-center justify-between w-full">
<PrimaryButton <PrimaryButton
onClick={handlePrevious} onClick={handlePrevious}
label="Previous" label={$t('course.navItem.lessons.exercises.all_exercises.previous')}
isDisabled={disablePreviousButton} isDisabled={disablePreviousButton}
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
/> />
@@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import { t } from '$lib/utils/functions/translations';
export let gradeMax = 0; export let gradeMax = 0;
export let disableGrading = false; export let disableGrading = false;
export let grade: number | undefined; export let grade: number | undefined;
@@ -8,7 +10,7 @@
{#if typeof grade !== 'undefined'} {#if typeof grade !== 'undefined'}
<div class="flex items-center"> <div class="flex items-center">
<TextField <TextField
placeholder="Points" placeholder={$t('course.navItem.lessons.exercises.new_exercise_modal.points')}
bind:value={grade} bind:value={grade}
type="number" type="number"
className="w-12" className="w-12"
@@ -5,6 +5,7 @@
import RadioItem from '$lib/components/Form/RadioItem.svelte'; import RadioItem from '$lib/components/Form/RadioItem.svelte';
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte'; import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
import Grade from '$lib/components/Question/Grade.svelte'; import Grade from '$lib/components/Question/Grade.svelte';
import { t } from '$lib/utils/functions/translations';
export let title = ''; export let title = '';
export let index = 1; export let index = 1;
@@ -103,7 +104,7 @@
<div class="mt-3 flex items-center justify-between w-full"> <div class="mt-3 flex items-center justify-between w-full">
<PrimaryButton <PrimaryButton
onClick={handlePrevious} onClick={handlePrevious}
label="Previous" label={$t('course.navItem.lessons.exercises.all_exercises.previous')}
isDisabled={disablePreviousButton} isDisabled={disablePreviousButton}
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
/> />
@@ -5,6 +5,7 @@
import TextArea from '$lib/components/Form/TextArea.svelte'; import TextArea from '$lib/components/Form/TextArea.svelte';
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte'; import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
import Grade from '$lib/components/Question/Grade.svelte'; import Grade from '$lib/components/Question/Grade.svelte';
import { t } from '$lib/utils/functions/translations';
export let title = ''; export let title = '';
export let index = 1; export let index = 1;
@@ -57,7 +58,11 @@
{defaultValue} {defaultValue}
</div> </div>
{:else} {:else}
<TextArea bind:value={defaultValue} rows="5" placeholder="Write your answer here" /> <TextArea
bind:value={defaultValue}
rows="5"
placeholder={$t('course.navItem.lessons.exercise.all_exercises.write_your_answer_here')}
/>
{/if} {/if}
</div> </div>
@@ -72,7 +77,9 @@
<PrimaryButton <PrimaryButton
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
type="submit" type="submit"
label={isLast ? 'Finish' : 'Next'} label={isLast
? $t('course.navItem.lessons.exercise.all_exercises.finish')
: $t('course.navItem.lessons.exercise.all_exercises.next')}
{name} {name}
/> />
</div> </div>
@@ -7,15 +7,15 @@ export const QUESTION_TYPE = {
export const QUESTION_TYPES = [ export const QUESTION_TYPES = [
{ {
id: QUESTION_TYPE.RADIO, id: QUESTION_TYPE.RADIO,
label: 'Single answer' label: 'course.navItem.lessons.exercises.all_exercises.edit_mode.question_types.single'
}, },
{ {
id: QUESTION_TYPE.CHECKBOX, id: QUESTION_TYPE.CHECKBOX,
label: 'Multiple answers' label: 'course.navItem.lessons.exercises.all_exercises.edit_mode.question_types.multiple'
}, },
{ {
id: QUESTION_TYPE.TEXTAREA, id: QUESTION_TYPE.TEXTAREA,
label: 'Paragraph' label: 'course.navItem.lessons.exercises.all_exercises.edit_mode.question_types.paragraph'
} }
]; ];
@@ -2,6 +2,7 @@
import TextField from '../Form/TextField.svelte'; import TextField from '../Form/TextField.svelte';
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte'; import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
import IconButton from '$lib/components/IconButton/index.svelte'; import IconButton from '$lib/components/IconButton/index.svelte';
import { t } from '$lib/utils/functions/translations';
export let isTitle = false; export let isTitle = false;
export let onClose = () => {}; export let onClose = () => {};
@@ -41,9 +42,9 @@
class="flex justify-between items-center border-gray border-t-2 border-r-0 border-b-0 border-l-0 p-2" class="flex justify-between items-center border-gray border-t-2 border-r-0 border-b-0 border-l-0 p-2"
> >
<div class="flex items-center w-40"> <div class="flex items-center w-40">
<p class="dark:text-white text-sm mr-2">Points:</p> <p class="dark:text-white text-sm mr-2">{$t('course.navItem.lessons.exercises.new_exercise_modal.points')}:</p>
<TextField <TextField
placeholder="Points" placeholder={$t('course.navItem.lessons.exercises.new_exercise_modal.points')}
bind:value={points} bind:value={points}
type="number" type="number"
onChange={onPointsChange} onChange={onPointsChange}
@@ -5,6 +5,7 @@
import { SNACKBAR_SEVERITY } from './constants'; import { SNACKBAR_SEVERITY } from './constants';
import { snackbarStore, snackbarStoreInitialState } from './store'; import { snackbarStore, snackbarStoreInitialState } from './store';
import { t } from '$lib/utils/functions/translations';
let timeoutId: NodeJS.Timeout | undefined; let timeoutId: NodeJS.Timeout | undefined;
let kind: 'error' | 'info' | 'info-square' | 'success' | 'warning' | 'warning-alt' = 'info'; let kind: 'error' | 'info' | 'info-square' | 'success' | 'warning' | 'warning-alt' = 'info';
@@ -41,8 +42,8 @@
$snackbarStore.severity === SNACKBAR_SEVERITY.SUCCESS $snackbarStore.severity === SNACKBAR_SEVERITY.SUCCESS
? 'success' ? 'success'
: $snackbarStore.severity === SNACKBAR_SEVERITY.ERROR : $snackbarStore.severity === SNACKBAR_SEVERITY.ERROR
? 'error' ? 'error'
: 'info'; : 'info';
</script> </script>
{#if $snackbarStore.open} {#if $snackbarStore.open}
@@ -51,7 +52,7 @@
<InlineNotification <InlineNotification
{kind} {kind}
title={capitalizeFirstLetter(kind || '')} title={capitalizeFirstLetter(kind || '')}
subtitle={$snackbarStore.message} subtitle={$t($snackbarStore.message)}
on:close={(e) => { on:close={(e) => {
e.preventDefault(); e.preventDefault();
handleClose(); handleClose();
@@ -12,7 +12,7 @@ export const snackbarStoreInitialState = {
export const snackbarStore = writable({ ...snackbarStoreInitialState }); export const snackbarStore = writable({ ...snackbarStoreInitialState });
export const snackbar = { export const snackbar = {
success(message = 'Success') { success(message = 'snackbar.success') {
snackbarStore.update((store) => { snackbarStore.update((store) => {
store.open = true; store.open = true;
store.message = message; store.message = message;
@@ -21,7 +21,7 @@ export const snackbar = {
return store; return store;
}); });
}, },
error(message = 'Something went wrong - Please try later') { error(message = 'snackbar.something') {
snackbarStore.update((store) => { snackbarStore.update((store) => {
store.open = true; store.open = true;
store.message = message; store.message = message;
@@ -30,7 +30,7 @@ export const snackbar = {
return store; return store;
}); });
}, },
info(message = 'Update') { info(message = 'snackbar.updates') {
snackbarStore.update((store) => { snackbarStore.update((store) => {
store.open = true; store.open = true;
store.message = message; store.message = message;
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import TextChip from '$lib/components/Chip/Text.svelte'; import TextChip from '$lib/components/Chip/Text.svelte';
import { t } from '$lib/utils/functions/translations';
interface Tab { interface Tab {
icon?: any; icon?: any;
@@ -8,8 +9,8 @@
badgeValue?: number; badgeValue?: number;
} }
export let tabs: Tab[] = []; export let tabs: Tab[] = [];
export let currentTab: string; export let currentTab: string | number;
export let onChange = (v: string) => () => {}; export let onChange = (v: string | number) => () => {};
</script> </script>
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
@@ -23,7 +24,7 @@
on:click={onChange(tab.value)} on:click={onChange(tab.value)}
> >
<div class="flex items-center justify-center w-full text-center"> <div class="flex items-center justify-center w-full text-center">
{tab.label} {$t(tab.label)}
</div> </div>
<span <span
class="absolute bottom-0 left-0 h-[2px] bg-primary-700 transition-all ease-in-out duration-500 {currentTab === class="absolute bottom-0 left-0 h-[2px] bg-primary-700 transition-all ease-in-out duration-500 {currentTab ===
@@ -43,13 +44,13 @@
{#if tab.icon} {#if tab.icon}
<svelte:component this={tab.icon} /> <svelte:component this={tab.icon} />
{/if} {/if}
{tab.label} {$t(tab.label)}
{#if typeof tab.badgeValue === 'number'} {#if typeof tab.badgeValue === 'number'}
<TextChip <TextChip
value={`${tab.badgeValue}`} value={`${tab.badgeValue}`}
size="sm" size="sm"
shape="rounded-full" shape="rounded-full"
className="bg-gray-300 dark:text-black text-xs absolute right-0 px-2" className="bg-gray-300 dark:text-black text-xs absolute -right-2 px-2"
/> />
{/if} {/if}
</div> </div>
@@ -15,6 +15,7 @@
const apiKey = PUBLIC_TINYMCE_API_KEY; const apiKey = PUBLIC_TINYMCE_API_KEY;
let unmount = false; let unmount = false;
let editorChangeHandlerId;
function getTinymce() { function getTinymce() {
const getSink = () => { const getSink = () => {
@@ -54,16 +55,20 @@
init_instance_callback: function (editor: any) { init_instance_callback: function (editor: any) {
editorWindowRef = editor.iframeElement?.contentWindow; editorWindowRef = editor.iframeElement?.contentWindow;
editor.on('Change', function () { editor.on('Paste Change input Undo Redo', function () {
const html = editor.getContent(); clearTimeout(editorChangeHandlerId);
if (onChange) {
onChange(html);
}
// backup in case the data doesn't get to our backend, we should store the note to avoid data loss editorChangeHandlerId = setTimeout(function () {
if (id) { console.log('changed');
localStorage.setItem(id, html); const html = editor.getContent();
} if (onChange) {
onChange(html);
}
if (id) {
localStorage.setItem(id, html);
}
}, 1000);
}); });
} }
}; };
@@ -18,6 +18,7 @@
import type { RealtimeChannel, RealtimePostgresChangesPayload } from '@supabase/supabase-js'; import type { RealtimeChannel, RealtimePostgresChangesPayload } from '@supabase/supabase-js';
import Confetti from '$lib/components/Confetti/index.svelte'; import Confetti from '$lib/components/Confetti/index.svelte';
import { toggleConfetti } from '$lib/components/Confetti/store'; import { toggleConfetti } from '$lib/components/Confetti/store';
import { t } from '$lib/utils/functions/translations';
const disabledClass = 'bg-gray-300 text-gray-400 hover:cursor-not-allowed'; const disabledClass = 'bg-gray-300 text-gray-400 hover:cursor-not-allowed';
@@ -48,7 +49,7 @@
}); });
if (!checkoutURL) { if (!checkoutURL) {
snackbar.error('Failed to generate success'); snackbar.error('upgrade.generate_fail');
return; return;
} }
@@ -56,7 +57,7 @@
} catch (error) { } catch (error) {
console.error('Error subscribing', error); console.error('Error subscribing', error);
snackbar.error('Failed, please try again later'); snackbar.error('upgrade.failed');
} }
isLoadingPlan = null; isLoadingPlan = null;
@@ -122,19 +123,26 @@
{open} {open}
width="w-4/5" width="w-4/5"
maxWidth={upgraded ? 'max-w-[600px]' : undefined} maxWidth={upgraded ? 'max-w-[600px]' : undefined}
modalHeading="Upgrade Plan" modalHeading={$t('pricing.modal.heading')}
> >
{#if upgraded} {#if upgraded}
<div class="flex flex-col items-center justify-center mb-4 w-full relative"> <div class="flex flex-col items-center justify-center mb-4 w-full relative">
<StepDoneIcon /> <StepDoneIcon />
<h4 class="text-2xl">Thank you for your support</h4> <h4 class="text-2xl">{$t('pricing.modal.thanks')}</h4>
<p class="mb-4 text-center"> <p class="mb-4 text-center">
Your plan comes with cool perks and benefits including access to all future features we {$t('pricing.modal.plan')}
release.
</p> </p>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<PrimaryButton label="Close" variant={VARIANTS.OUTLINED} onClick={onClose} /> <PrimaryButton
<PrimaryButton label="Learn more" variant={VARIANTS.CONTAINED_DARK} onClick={onLearnMore} /> label={$t('pricing.modal.close')}
variant={VARIANTS.OUTLINED}
onClick={onClose}
/>
<PrimaryButton
label={$t('pricing.modal.learn')}
variant={VARIANTS.CONTAINED_DARK}
onClick={onLearnMore}
/>
</div> </div>
</div> </div>
{:else} {:else}
@@ -150,7 +158,7 @@
? 'text-white' ? 'text-white'
: 'text-gray-900'} dark:text-white lg:text-xl" : 'text-gray-900'} dark:text-white lg:text-xl"
> >
{PLANS[planName].NAME} {$t(PLANS[planName].NAME)}
</p> </p>
<p <p
class="text-baseline flex items-baseline gap-x-1 font-medium {planName === class="text-baseline flex items-baseline gap-x-1 font-medium {planName ===
@@ -159,9 +167,9 @@
: 'text-black'} dark:text-gray-300" : 'text-black'} dark:text-gray-300"
> >
{PLANS[planName].PRICE.CURRENCY} {PLANS[planName].PRICE.CURRENCY}
{PLANS[planName].PRICE.MONTHLY} {$t(PLANS[planName].PRICE.MONTHLY)}
{#if !PLANS[planName].PRICE.IS_PREMIUM} {#if !PLANS[planName].PRICE.IS_PREMIUM}
/month /{$t('pricing.modal.month')}
{/if} {/if}
</p> </p>
<p <p
@@ -169,7 +177,7 @@
? 'text-white' ? 'text-white'
: 'text-black'} dark:text-gray-300 lg:leading-6" : 'text-black'} dark:text-gray-300 lg:leading-6"
> >
{PLANS[planName].DESCRIPTION} {$t(PLANS[planName].DESCRIPTION)}
</p> </p>
<button <button
@@ -188,7 +196,7 @@
{#if isLoadingPlan === planName} {#if isLoadingPlan === planName}
<Loading withOverlay={false} small /> <Loading withOverlay={false} small />
{:else} {:else}
{PLANS[planName].CTA.DASHBOARD_LABEL} {$t(PLANS[planName].CTA.DASHBOARD_LABEL)}
{/if} {/if}
</button> </button>
@@ -207,7 +215,7 @@
/> />
</div> </div>
<p class="text-sm"> <p class="text-sm">
{features} {$t(features)}
</p> </p>
</li> </li>
{/each} {/each}
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import Camera from 'carbon-icons-svelte/lib/Camera.svelte'; import Camera from 'carbon-icons-svelte/lib/Camera.svelte';
import { t } from '$lib/utils/functions/translations';
export let avatar: string | undefined; export let avatar: string | undefined;
export let src: string | undefined; export let src: string | undefined;
@@ -45,7 +46,8 @@
fileinput.click(); fileinput.click();
}} }}
> >
<Camera size={20} /> <span class="ml-2">Upload Image</span> <Camera size={20} />
<span class="ml-2">{$t('settings.profile.profile_picture.upload_image')}</span>
</button> </button>
{#if errorMessage} {#if errorMessage}
<p class="text-sm text-red-500">{errorMessage}</p> <p class="text-sm text-red-500">{errorMessage}</p>
@@ -8,6 +8,7 @@
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store'; import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
import { queryUnsplash } from './utils'; import { queryUnsplash } from './utils';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { t } from '$lib/utils/functions/translations';
export let imageURL = ''; export let imageURL = '';
@@ -33,7 +34,7 @@
}[] = []; }[] = [];
let fileInput: HTMLInputElement; let fileInput: HTMLInputElement;
let label = "Upload Image" let label = $t('snackbar.landing_page_settings.error.label');
const onChange = (tabValue: string) => () => (currentTab = tabValue); const onChange = (tabValue: string) => () => (currentTab = tabValue);
@@ -45,9 +46,9 @@
const onFileSelected = () => { const onFileSelected = () => {
const file = fileInput?.files?.[0]; const file = fileInput?.files?.[0];
const sizeInkb = file?.size! / 1024; const sizeInkb = file?.size! / 1024;
if (sizeInkb > 500){ if (sizeInkb > 500) {
snackbar.error("File Size must not be greater than 500kb") snackbar.error('snackbar.landing_page_settings.error.file_size');
label = "Try Again" label = $t('snackbar.landing_page_settings.error.try_again');
return; return;
} }
if (file) { if (file) {
@@ -76,7 +77,7 @@
} }
isUploading = false; isUploading = false;
snackbar.success(`Complete :)`); snackbar.success(`snackbar.landing_page_settings.success.complete`);
$handleOpenWidget.open = false; $handleOpenWidget.open = false;
}; };
@@ -88,7 +89,7 @@
try { try {
unsplashImages = await queryUnsplash(searchQuery || 'rocks'); unsplashImages = await queryUnsplash(searchQuery || 'rocks');
} catch (error) { } catch (error) {
snackbar.error('Error fetching images from Unsplash'); snackbar.error('snackbar.landing_page_settings.error.fetch_error');
console.error('Error fetching images from Unsplash:', error); console.error('Error fetching images from Unsplash:', error);
} }
} }
@@ -101,7 +102,7 @@
bind:open={$handleOpenWidget.open} bind:open={$handleOpenWidget.open}
width="w-3/5" width="w-3/5"
maxWidth="" maxWidth=""
modalHeading="Banner Widget" modalHeading={$t('course.navItem.landing_page.upload_widget.title')}
> >
<div class="w-full bg-white dark:bg-inherit p-5"> <div class="w-full bg-white dark:bg-inherit p-5">
<Tabs {tabs} {currentTab} {onChange}> <Tabs {tabs} {currentTab} {onChange}>
@@ -117,15 +118,17 @@
disabled={isUploading} disabled={isUploading}
/> />
<PrimaryButton <PrimaryButton
label={label} {label}
onClick={handleUpload} onClick={handleUpload}
isLoading={isUploading} isLoading={isUploading}
className="w-full font-semibold m-auto" className="w-full font-semibold m-auto"
/> />
<p class="text-center text-sm text-gray-500 my-2"> <p class="text-center text-sm text-gray-500 my-2">
Images wider than 1500 pixels work best. {$t('course.navItem.landing_page.upload_widget.width')}
</p>
<p class="text-center text-sm text-gray-500">
{$t('course.navItem.landing_page.upload_widget.size')}
</p> </p>
<p class="text-center text-sm text-gray-500">The maximum size per file is 500kb.</p>
</div> </div>
</TabContent> </TabContent>
<TabContent value={tabs[0].value} index={currentTab}> <TabContent value={tabs[0].value} index={currentTab}>
@@ -142,7 +145,7 @@
<button <button
type="submit" type="submit"
class="px-3 py-1 bg-white rounded-lg border-[1px] border-gray-500 text-black" class="px-3 py-1 bg-white rounded-lg border-[1px] border-gray-500 text-black"
>Submit</button >{$t('course.navItem.landing_page.upload_widget.submit')}</button
> >
</form> </form>
{#if unsplashImages && unsplashImages.length > 0} {#if unsplashImages && unsplashImages.length > 0}
@@ -173,7 +176,9 @@
{/each} {/each}
</div> </div>
{:else} {:else}
<p class="text-center pt-7">No images available.</p> <p class="text-center pt-7">
{$t('course.navItem.landing_page.upload_widget.no_images')}
</p>
{/if} {/if}
</div> </div>
</TabContent> </TabContent>

Some files were not shown because too many files have changed in this diff Show More