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

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