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:
co-authored by
Rotimi Best
parent
ff7d2cee35
commit
d75e2f258e
@@ -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 > 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>
|
||||
|
||||
+6
-3
@@ -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
-2
@@ -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>
|
||||
|
||||
+5
-5
@@ -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>
|
||||
|
||||
+7
-4
@@ -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;
|
||||
|
||||
+22
-9
@@ -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}
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@
|
||||
total
|
||||
});
|
||||
|
||||
snackbar.success(`Submission updated to '${status.label}'`);
|
||||
snackbar.success(`snackbar.exercise.submission_updated '${status.label}'`);
|
||||
}
|
||||
|
||||
function setStatus(data) {
|
||||
|
||||
+2
-1
@@ -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={{
|
||||
|
||||
+5
-2
@@ -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}
|
||||
|
||||
+4
-1
@@ -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>
|
||||
|
||||
+13
-3
@@ -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}
|
||||
|
||||
+17
-9
@@ -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>
|
||||
|
||||
+51
-29
@@ -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>
|
||||
+9
-3
@@ -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
-2
@@ -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>
|
||||
|
||||
+28
-13
@@ -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}
|
||||
|
||||
+9
-3
@@ -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>
|
||||
|
||||
+4
-3
@@ -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
|
||||
}
|
||||
];
|
||||
|
||||
+25
-3
@@ -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;
|
||||
}
|
||||
|
||||
+184
-79
@@ -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>
|
||||
|
||||
+5
-4
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user