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
@@ -1,8 +1,9 @@
|
||||
import { dev } from '$app/environment';
|
||||
import { dev, browser } from '$app/environment';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { blockedSubdomain } from '$lib/utils/constants/app';
|
||||
import { getCurrentOrg } from '$lib/utils/services/org';
|
||||
import { getSupabase, supabase } from '$lib/utils/functions/supabase';
|
||||
import { loadTranslations } from '$lib/utils/functions/translations';
|
||||
import type { CurrentOrg } from '$lib/utils/types/org';
|
||||
|
||||
if (!supabase) {
|
||||
@@ -53,5 +54,23 @@ export const load = async ({ url, cookies }): Promise<LoadOutput> => {
|
||||
throw redirect(307, 'https://app.classroomio.com');
|
||||
}
|
||||
|
||||
// Load translations
|
||||
const { pathname } = url;
|
||||
const initLocale = getInitialLocale();
|
||||
await loadTranslations(initLocale, pathname);
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
// Define getInitialLocale function
|
||||
function getInitialLocale(): string {
|
||||
if (browser) {
|
||||
try {
|
||||
return window.navigator.language.split('-')[0];
|
||||
} catch(e) {
|
||||
return 'en';
|
||||
}
|
||||
}
|
||||
|
||||
return 'en';
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
import { identifyPosthogUser, initPosthog } from '$lib/utils/services/posthog';
|
||||
import { initSentry, setSentryUser } from '$lib/utils/services/sentry';
|
||||
import UpgradeModal from '$lib/components/Upgrade/Modal.svelte';
|
||||
import { handleLocaleChange } from '$lib/utils/functions/translations';
|
||||
|
||||
import '../app.postcss';
|
||||
|
||||
@@ -124,6 +125,9 @@
|
||||
|
||||
setAnalyticsUser();
|
||||
|
||||
// Fetch language
|
||||
handleLocaleChange($profile.locale);
|
||||
|
||||
if (data.isOrgSite) {
|
||||
const { data, error } = await supabase
|
||||
.from('organizationmember')
|
||||
@@ -166,6 +170,9 @@
|
||||
// Set user in sentry
|
||||
setAnalyticsUser();
|
||||
|
||||
// Fetch language
|
||||
handleLocaleChange($profile.locale);
|
||||
|
||||
const orgRes = await getOrganizations(profileData.id, data.isOrgSite, data.orgSiteName);
|
||||
|
||||
// student redirect
|
||||
|
||||
@@ -21,7 +21,8 @@ const instruction = {
|
||||
|
||||
export async function POST({ request }) {
|
||||
const { prompt } = await request.json();
|
||||
const { courseTitle, lessonTitle, type } = JSON.parse(prompt);
|
||||
const { courseTitle, lessonTitle, type, locale } = JSON.parse(prompt);
|
||||
|
||||
const response = await openai.createChatCompletion({
|
||||
model: 'gpt-3.5-turbo',
|
||||
messages: [
|
||||
@@ -31,7 +32,7 @@ export async function POST({ request }) {
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Generate ${instruction[type]} given the course title is "${courseTitle}" and the title of the lesson is "${lessonTitle}". Format in HTML without any styling. MOST IMPORTANT DON'T include the title of the course and don't include the lesson title: "${lessonTitle}" in your output. Please make sure the content is well detailed.`
|
||||
content: `Generate ${instruction[type]} given the course title is "${courseTitle}" and the title of the lesson is "${lessonTitle}". Format in HTML without any styling. MOST IMPORTANT DON'T include the title of the course and don't include the lesson title: "${lessonTitle}" in your output. Please make sure the content is well detailed and you output the content in this locale: ${locale}`
|
||||
}
|
||||
],
|
||||
stream: true
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { selectedLocale } from '$lib/utils/functions/translations';
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const POST = (async ({ request }) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
selectedLocale.set(body.locale);
|
||||
} catch (e) {
|
||||
selectedLocale.set('en');
|
||||
}
|
||||
|
||||
return json({ status: 200 });
|
||||
}) satisfies RequestHandler;
|
||||
@@ -30,6 +30,8 @@
|
||||
triggerSendEmail
|
||||
} from '$lib/utils/services/notification/notification';
|
||||
import NewsFeedLoader from '$lib/components/Course/components/NewsFeed/NewsFeedLoader.svelte';
|
||||
import { t } from '$lib/utils/functions/translations.js';
|
||||
|
||||
export let data;
|
||||
|
||||
let createdComment;
|
||||
@@ -65,7 +67,7 @@
|
||||
comment: feed.comment.filter((comment) => comment.id !== id)
|
||||
}));
|
||||
|
||||
return snackbar.success('Comment Deleted');
|
||||
return snackbar.success('snackbar.course.success.comment_deleted');
|
||||
};
|
||||
|
||||
const addNewReaction = async (reactionType: string, feedId: string, authorId: string) => {
|
||||
@@ -98,7 +100,7 @@
|
||||
.eq('id', feedId);
|
||||
|
||||
if (response.error) {
|
||||
return snackbar.error('An error occurred while reacting to news feed');
|
||||
return snackbar.error('snackbar.course.error.reaction_error');
|
||||
}
|
||||
|
||||
$newsFeed = $newsFeed.map((feed) => {
|
||||
@@ -118,7 +120,7 @@
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
return snackbar.error('An error occurred while creating comment');
|
||||
return snackbar.error('snackbar.course.error.commenting_error');
|
||||
}
|
||||
|
||||
if (!response.data) return;
|
||||
@@ -142,7 +144,7 @@
|
||||
content: comment
|
||||
};
|
||||
|
||||
snackbar.success('Comment added');
|
||||
snackbar.success('snackbar.course.success.comment_added');
|
||||
|
||||
return {
|
||||
...feed,
|
||||
@@ -159,12 +161,16 @@
|
||||
const { response } = await toggleFeedIsPinned(feedId, newIsPinned);
|
||||
|
||||
if (response.error) {
|
||||
return snackbar.error('Failed to toggle pinned feed');
|
||||
return snackbar.error('snackbar.course.error.toggle_error');
|
||||
}
|
||||
|
||||
$newsFeed = $newsFeed.map((feed) => {
|
||||
if (feed.id === feedId) {
|
||||
snackbar.success(`${newIsPinned ? 'Pinned' : 'Unpinned'} Successfully`);
|
||||
snackbar.success(
|
||||
`${
|
||||
newIsPinned ? 'snackbar.course.success.pinned' : 'snackbar.course.success.unpinned'
|
||||
} snackbar.course.success.successfully`
|
||||
);
|
||||
|
||||
feed.isPinned = newIsPinned;
|
||||
return feed;
|
||||
@@ -178,7 +184,7 @@
|
||||
deleteNewsFeed(id);
|
||||
|
||||
const deletedFeed = $newsFeed.filter((feed) => feed.id !== id);
|
||||
snackbar.success('Feed deleted successfully');
|
||||
snackbar.success('snackbar.course.success.feed_delete_success');
|
||||
|
||||
$newsFeed = deletedFeed;
|
||||
};
|
||||
@@ -189,7 +195,7 @@
|
||||
isLoading = true;
|
||||
const { data, error } = await fetchNewsFeeds(courseId);
|
||||
if (error) {
|
||||
snackbar.error('Failed to fetch news feeds');
|
||||
snackbar.error('snackbar.course.error.news_feed_fail');
|
||||
}
|
||||
if (data) {
|
||||
$newsFeed = data.map((feedItem) => ({
|
||||
@@ -198,7 +204,7 @@
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
snackbar.error('An error occured while fetching feed');
|
||||
snackbar.error('snackbar.course.error.feed_fetch_error');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
@@ -223,10 +229,14 @@
|
||||
</script>
|
||||
|
||||
<CourseContainer bind:courseId={data.courseId}>
|
||||
<PageNav title="News Feed" disableSticky={true}>
|
||||
<PageNav title={$t('course.navItem.news_feed.heading')} disableSticky={true}>
|
||||
<slot:fragment slot="widget">
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<PrimaryButton className="mr-2" label="Add" onClick={() => ($isNewFeedModal.open = true)} />
|
||||
<PrimaryButton
|
||||
className="mr-2"
|
||||
label={$t('course.navItem.news_feed.heading_button.title')}
|
||||
onClick={() => ($isNewFeedModal.open = true)}
|
||||
/>
|
||||
</RoleBasedSecurity>
|
||||
</slot:fragment>
|
||||
</PageNav>
|
||||
@@ -254,9 +264,9 @@
|
||||
<Box>
|
||||
<div class="flex justify-between flex-col items-center w-[90%] md:w-96">
|
||||
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="my-2.5 mx-auto" />
|
||||
<h2 class="text-xl my-1.5 font-normal">No post yet</h2>
|
||||
<h2 class="text-xl my-1.5 font-normal">{$t('course.navItem.news_feed.body_header')}</h2>
|
||||
<p class="text-sm text-center text-slate-500">
|
||||
Make a post to your class. Start by clicking on the Add button.
|
||||
{$t('course.navItem.news_feed.body_content')}
|
||||
</p>
|
||||
</div>
|
||||
</Box>
|
||||
@@ -266,7 +276,7 @@
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<PinFilled size={16} />
|
||||
|
||||
<p class="text-sm">Pinned</p>
|
||||
<p class="text-sm">{$t('course.navItem.news_feed.pinned')}</p>
|
||||
</div>
|
||||
{/if}
|
||||
<NewsFeedCard
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import type { GroupPerson, Lesson } from '$lib/utils/types/index';
|
||||
import { browser } from '$app/environment';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { t } from '$lib/utils/functions/translations.js';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -91,7 +92,7 @@
|
||||
takeAttendance(_data).then((res) => {
|
||||
if (res.error) {
|
||||
console.error(`res.error`, res.error);
|
||||
snackbar.error("Something isn't right. Please reload and take attendance again");
|
||||
snackbar.error('snackbar.attendance.error.reload');
|
||||
} else {
|
||||
// console.log('res', JSON.stringify(res));
|
||||
const { id, is_present } = res.data[0];
|
||||
@@ -137,7 +138,7 @@
|
||||
</script>
|
||||
|
||||
<CourseContainer bind:courseId={data.courseId}>
|
||||
<PageNav title="Attendance" />
|
||||
<PageNav title={$t('course.navItem.attendance.title')} />
|
||||
<PageBody width="w-full max-w-6xl md:w-11/12">
|
||||
<section class="flex items-center mx-2 lg:mx-9 my-5">
|
||||
<div
|
||||
@@ -145,16 +146,18 @@
|
||||
>
|
||||
<div class="flex">
|
||||
<p class="flex items-center mr-5">
|
||||
<Checkbox checked disabled /> Present
|
||||
<Checkbox checked disabled />
|
||||
{$t('course.navItem.attendance.present')}
|
||||
</p>
|
||||
<p class="flex items-center">
|
||||
<Checkbox disabled /> Absent
|
||||
<Checkbox disabled />
|
||||
{$t('course.navItem.attendance.absent')}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Search
|
||||
class="dark:text-slate-950"
|
||||
placeholder="Search students"
|
||||
placeholder={$t('course.navItem.attendance.search_students')}
|
||||
bind:value={searchValue}
|
||||
/>
|
||||
</div>
|
||||
@@ -166,10 +169,14 @@
|
||||
<!-- Moved the lesson headers outside the students loop -->
|
||||
<StructuredListHead class="bg-primary-100">
|
||||
<StructuredListRow head class="mx-7">
|
||||
<StructuredListCell head class="text-primary-600 py-3">Student</StructuredListCell>
|
||||
<StructuredListCell head class="text-primary-600 py-3"
|
||||
>{$t('course.navItem.attendance.student')}</StructuredListCell
|
||||
>
|
||||
{#each $lessons as lesson, index}
|
||||
<StructuredListCell head class="text-primary-600 py-3"
|
||||
>Lesson 0{getLectureNo(index + 1)}</StructuredListCell
|
||||
>{$t('course.navItem.attendance.lesson')} 0{getLectureNo(
|
||||
index + 1
|
||||
)}</StructuredListCell
|
||||
>
|
||||
{/each}
|
||||
</StructuredListRow>
|
||||
@@ -206,7 +213,9 @@
|
||||
{#if students.length === 0}
|
||||
<Box className="h-[300px] w-full">
|
||||
<AudioConsoleIcon size={32} class="carbon-icon w-80" />
|
||||
<h3 class="text-3xl text-gray-500 dark:text-white text-center">No Student Added</h3>
|
||||
<h3 class="text-3xl text-gray-500 dark:text-white text-center">
|
||||
{$t('course.navItem.attendance.no_student')}
|
||||
</h3>
|
||||
</Box>
|
||||
{/if}
|
||||
{#if students.length !== 0}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// import { Tabs, Tab } from 'carbon-components-svelte';
|
||||
// import TabContent from 'carbon-components-svelte/src/Tabs/TabContent.svelte';
|
||||
import StudentCertificate from '$lib/components/Course/components/Ceritficate/StudentCertificate/Index.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
export let courseId: string = '';
|
||||
</script>
|
||||
@@ -18,7 +19,7 @@
|
||||
<IssueCertificateModal />
|
||||
|
||||
<CourseContainer bind:courseId>
|
||||
<PageNav title="Certificates" disableSticky={true}>
|
||||
<PageNav title={$t('course.navItem.certificates.title')} disableSticky={true}>
|
||||
<slot:fragment slot="widget">
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<!-- Hide Functionality For V1 -->
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
import { course, group } from '$lib/components/Course/store';
|
||||
import DateField from '$lib/components/Form/Date.svelte';
|
||||
import type { Lesson } from '$lib/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations.js';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let data;
|
||||
@@ -123,10 +124,14 @@
|
||||
|
||||
<!-- TODO: Refactor usage of two way binding isStudent, rather use $globalStore.isStudent -->
|
||||
<CourseContainer bind:isFetching bind:isStudent bind:courseId={data.courseId}>
|
||||
<PageNav title="Lessons">
|
||||
<PageNav title={$t('course.navItem.lessons.heading')}>
|
||||
<div slot="widget">
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<PrimaryButton label="Add" onClick={addLesson} isDisabled={!!lessonEditing} />
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.add_lesson.button_title')}
|
||||
onClick={addLesson}
|
||||
isDisabled={!!lessonEditing}
|
||||
/>
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
</PageNav>
|
||||
@@ -206,7 +211,9 @@
|
||||
className="sm:my-1 w-[100%]"
|
||||
/>
|
||||
{:else if !lesson.profile}
|
||||
<p class="mb-3 ml-2 text-sm dark:text-white">No tutor added</p>
|
||||
<p class="dark:text-white ml-2 text-sm mb-3">
|
||||
{$t('course.navItem.lessons.no_tutor')}
|
||||
</p>
|
||||
{:else}
|
||||
<a href="." class="mb-2 flex items-center hover:underline">
|
||||
<Avatar
|
||||
@@ -256,7 +263,9 @@
|
||||
href={lesson.call_url || '#'}
|
||||
target="_blank"
|
||||
>
|
||||
{lesson.call_url ? 'Join lesson' : 'No link'}
|
||||
{lesson.call_url
|
||||
? $t('course.navItem.lessons.join_lesson')
|
||||
: $t('course.navItem.lessons.no_link')}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -270,14 +279,18 @@
|
||||
<OverflowMenu size="xl" class="absolute right-0 top-0">
|
||||
<OverflowMenuItem
|
||||
disabled={isStudent}
|
||||
text={lesson.is_unlocked ? 'Lock' : 'Unlock'}
|
||||
text={lesson.is_unlocked
|
||||
? $t('course.navItem.lessons.add_lesson.lock')
|
||||
: $t('course.navItem.lessons.add_lesson.unlock')}
|
||||
on:click={() => {
|
||||
lesson.is_unlocked = !lesson.is_unlocked;
|
||||
handleSaveLesson(lesson, $course.id);
|
||||
}}
|
||||
/>
|
||||
<OverflowMenuItem
|
||||
text={lessonEditing === lesson.id ? 'Save' : 'Edit'}
|
||||
text={lessonEditing === lesson.id
|
||||
? $t('course.navItem.lessons.add_lesson.save')
|
||||
: $t('course.navItem.lessons.add_lesson.edit')}
|
||||
on:click={() => {
|
||||
if (lessonEditing === lesson.id) {
|
||||
lessonEditing = undefined;
|
||||
@@ -287,7 +300,11 @@
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<OverflowMenuItem danger text="Delete" on:click={() => handleDelete(lesson.id)} />
|
||||
<OverflowMenuItem
|
||||
danger
|
||||
text={$t('course.navItem.lessons.add_lesson.delete')}
|
||||
on:click={() => handleDelete(lesson.id)}
|
||||
/>
|
||||
</OverflowMenu>
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
@@ -296,10 +313,9 @@
|
||||
<Box>
|
||||
<div class="flex justify-between flex-col items-center w-[90%] md:w-96">
|
||||
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="my-2.5 mx-auto" />
|
||||
<h2 class="text-xl my-1.5 font-normal">No lessons yet</h2>
|
||||
<h2 class="text-xl my-1.5 font-normal">{$t('course.navItem.lessons.body_header')}</h2>
|
||||
<p class="text-sm text-center text-slate-500">
|
||||
Share your knowledge with the world by creating engaging lessons. Start by clicking
|
||||
on the Add button.
|
||||
{$t('course.navItem.lessons.body_content')}
|
||||
</p>
|
||||
</div>
|
||||
</Box>
|
||||
@@ -307,12 +323,11 @@
|
||||
</section>
|
||||
{:else}
|
||||
<Box className="w-full lg:w-11/12 lg:px-4 m-auto">
|
||||
<div class="flex flex-col items-center justify-between">
|
||||
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="mx-auto my-2.5" />
|
||||
<h2 class="my-1.5 text-xl font-normal">No lessons yet</h2>
|
||||
<p class="text-center text-sm text-slate-500">
|
||||
Share your knowledge with the world by creating engaging lessons. Start by clicking on
|
||||
the Add button.
|
||||
<div class="flex justify-between flex-col items-center">
|
||||
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="my-2.5 mx-auto" />
|
||||
<h2 class="text-xl my-1.5 font-normal">{$t('course.navItem.lessons.body_header')}</h2>
|
||||
<p class="text-sm text-center text-slate-500">
|
||||
{$t('course.navItem.lessons.body_content')}
|
||||
</p>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import CourseIcon from '$lib/components/Icons/CourseIcon.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { Loading } from 'carbon-components-svelte';
|
||||
import { Dropdown, Loading } from 'carbon-components-svelte';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import PageNav from '$lib/components/PageNav/index.svelte';
|
||||
import PageBody from '$lib/components/PageBody/index.svelte';
|
||||
@@ -25,22 +25,25 @@
|
||||
import { apps } from '$lib/components/Apps/store';
|
||||
import APPS_CONSTANTS from '$lib/components/Apps/constants';
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import { lesson, lessons } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import {
|
||||
lesson,
|
||||
lessons,
|
||||
lessonByTranslation
|
||||
} from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import { browser } from '$app/environment';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import type { LessonCompletion } from '$lib/utils/types';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { getIsLessonComplete } from '$lib/components/Course/components/Lesson/functions';
|
||||
import { captureMessage } from '@sentry/browser';
|
||||
import { t } from '$lib/utils/functions/translations.js';
|
||||
import { LANGUAGES } from '$lib/utils/constants/translation';
|
||||
|
||||
export let data;
|
||||
|
||||
let path = '';
|
||||
let mode = MODES.view;
|
||||
let prevMode = '';
|
||||
let prevLessonId = '';
|
||||
let isFetching = false;
|
||||
let isMarkingComplete = false;
|
||||
let isLoading = false;
|
||||
let isSaving = false;
|
||||
@@ -58,21 +61,19 @@
|
||||
}
|
||||
|
||||
async function fetchReqData(lessonId = '', isMaterialsTabActive: boolean) {
|
||||
isFetching = true;
|
||||
|
||||
$lesson.isFetching = true;
|
||||
let lessonData;
|
||||
if (isMaterialsTabActive) {
|
||||
const lesson = await fetchLesson(lessonId);
|
||||
lessonData = lesson.data;
|
||||
}
|
||||
|
||||
console.log('lessonData', lessonData);
|
||||
prevLessonId = lessonId;
|
||||
console.log({ lessonData });
|
||||
|
||||
const totalExercises = lessonData?.totalExercises?.[0] && lessonData.totalExercises[0].count;
|
||||
const totalComments = lessonData?.totalComments?.[0] && lessonData.totalComments[0].count;
|
||||
setLesson(lessonData, totalExercises || 0, totalComments || 0);
|
||||
isFetching = false;
|
||||
$lesson.isFetching = false;
|
||||
}
|
||||
|
||||
async function markLessonComplete(lessonId: string) {
|
||||
@@ -118,7 +119,7 @@
|
||||
|
||||
return l;
|
||||
});
|
||||
snackbar.success('Marked as complete');
|
||||
snackbar.success('snackbar.lessons.success.complete_marked');
|
||||
isMarkingComplete = false;
|
||||
}
|
||||
|
||||
@@ -168,10 +169,10 @@
|
||||
a.click();
|
||||
a.remove();
|
||||
|
||||
snackbar.success('Download Complete');
|
||||
snackbar.success('snackbar.lessons.success.complete_download');
|
||||
} catch (error) {
|
||||
console.log('error downloading lesson', error);
|
||||
snackbar.error("Something's not right - Please try later");
|
||||
snackbar.error('snackbar.lessons.error.try_later');
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
@@ -186,6 +187,7 @@
|
||||
lesson_completion = [...lessonData.lesson_completion];
|
||||
}
|
||||
|
||||
console.log('profile locale', $profile.locale);
|
||||
lesson.update((l) => ({
|
||||
...l,
|
||||
id: data.lessonId,
|
||||
@@ -197,8 +199,26 @@
|
||||
slide_url: lessonData.slide_url
|
||||
},
|
||||
lesson_completion,
|
||||
exercises: []
|
||||
exercises: [],
|
||||
locale: $profile.locale
|
||||
}));
|
||||
|
||||
if (Array.isArray(lessonData.lesson_language)) {
|
||||
lessonByTranslation.update((lessLocales) => {
|
||||
return {
|
||||
...lessLocales,
|
||||
[data.lessonId]: lessonData.lesson_language.reduce(
|
||||
(acc, cur) => {
|
||||
acc[cur.locale] = cur.content;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
en: ''
|
||||
}
|
||||
)
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const toggleApps = () => {
|
||||
@@ -229,19 +249,19 @@
|
||||
<CourseContainer
|
||||
bind:isStudent
|
||||
{path}
|
||||
isExercisePage={!data.isMaterialsTabActive && data.exerciseId}
|
||||
isExercisePage={!data.isMaterialsTabActive && !!data.exerciseId}
|
||||
bind:courseId={data.courseId}
|
||||
>
|
||||
<PageNav
|
||||
bind:hideOnMobile={isStudent}
|
||||
navItems={[
|
||||
{
|
||||
label: 'Materials',
|
||||
label: $t('course.navItem.lessons.lesson_nav.materials'),
|
||||
isActive: data.isMaterialsTabActive,
|
||||
href: path
|
||||
},
|
||||
{
|
||||
label: 'Exercises',
|
||||
label: $t('course.navItem.lessons.lesson_nav.exercises'),
|
||||
badgeValue: data.isMaterialsTabActive ? $lesson.totalExercises : $lesson.exercises.length,
|
||||
isActive: !data.isMaterialsTabActive,
|
||||
href: `${path}/exercises`
|
||||
@@ -249,43 +269,52 @@
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="widget">
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
{#if data.isMaterialsTabActive}
|
||||
<div class="tab">
|
||||
<IconButton onClick={toggleApps} buttonClassName="">
|
||||
<OverflowMenuVertical size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div
|
||||
class={`flex-row ${
|
||||
$apps.dropdown && $apps.open
|
||||
? 'absolute lg:relative top-[85px] lg:top-0 right-14 lg:right-0 z-40 dark:bg-neutral-800 p-3 lg:p-0'
|
||||
: 'hidden'
|
||||
} lg:flex items-center`}
|
||||
>
|
||||
{#if $course.metadata.lessonDownload && !!PUBLIC_SERVER_URL}
|
||||
<div class="flex">
|
||||
<div class="mr-5">
|
||||
<Dropdown items={LANGUAGES} bind:selectedId={$lesson.locale} class="h-full" />
|
||||
</div>
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
{#if data.isMaterialsTabActive}
|
||||
<div class="tab">
|
||||
<IconButton onClick={toggleApps} buttonClassName="">
|
||||
<OverflowMenuVertical size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div
|
||||
class={`flex-row ${
|
||||
$apps.dropdown && $apps.open
|
||||
? 'absolute lg:relative top-[85px] lg:top-0 right-14 lg:right-0 z-40 rounded-md bg-gray-100 dark:bg-neutral-800 p-3 lg:p-0'
|
||||
: 'hidden'
|
||||
} lg:flex items-center`}
|
||||
>
|
||||
<PrimaryButton
|
||||
className="mb-2 lg:mb-0 mr-2"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={downloadLesson}
|
||||
{isLoading}
|
||||
onClick={() => {
|
||||
$apps.dropdown = false;
|
||||
toggleMode();
|
||||
}}
|
||||
isDisabled={isSaving}
|
||||
>
|
||||
<Download size={16} class="mr-2" />
|
||||
Download PDF
|
||||
{mode === MODES.edit
|
||||
? $t('course.navItem.lessons.done')
|
||||
: $t('course.navItem.lessons.edit')}
|
||||
</PrimaryButton>
|
||||
{/if}
|
||||
|
||||
<PrimaryButton className="mr-2" variant={VARIANTS.OUTLINED} onClick={toggleMode}>
|
||||
{#if isSaving}
|
||||
<Loading withOverlay={false} small />
|
||||
<span class="text-sm ml-2 italic">Autosaving...</span>
|
||||
{:else}
|
||||
{mode === MODES.edit ? 'Done' : 'Edit'}
|
||||
{#if $course.metadata.lessonDownload && !!PUBLIC_SERVER_URL}
|
||||
<PrimaryButton
|
||||
className="mr-"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={downloadLesson}
|
||||
{isLoading}
|
||||
>
|
||||
<Download size={16} />
|
||||
</PrimaryButton>
|
||||
{/if}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
{/if}
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
{/if}
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</PageNav>
|
||||
|
||||
@@ -330,7 +359,10 @@
|
||||
{:else}
|
||||
<CheckmarkOutlineIcon size={24} class="carbon-icon mr-2" />
|
||||
{/if}
|
||||
Mark as {isLessonComplete ? 'Incomplete' : 'Complete'}
|
||||
{$t('course.navItem.lessons.mark_as')}
|
||||
{isLessonComplete
|
||||
? $t('course.navItem.lessons.incomplete')
|
||||
: $t('course.navItem.lessons.complete')}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { t } from '$lib/utils/functions/translations.js';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -38,7 +39,7 @@
|
||||
const { data: marks, error } = await fetchMarks(courseId);
|
||||
if (error) {
|
||||
console.error('Error fetching marks', error);
|
||||
snackbar.error('Error fetching marks');
|
||||
snackbar.error('snackbar.marks.error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -86,13 +87,13 @@
|
||||
</script>
|
||||
|
||||
<CourseContainer bind:courseId={data.courseId}>
|
||||
<PageNav title="Marks" />
|
||||
<PageNav title={$t('course.navItem.marks.title')} />
|
||||
|
||||
<PageBody width="w-full max-w-6xl md:w-11/12">
|
||||
<div class="table rounded-md border border-gray-300 w-full">
|
||||
<div class="flex items-center {borderBottomGrey}">
|
||||
<div class="box flex items-center p-3">
|
||||
<p class="dark:text-white w-40">Student</p>
|
||||
<p class="dark:text-white w-40">{$t('course.navItem.marks.student')}</p>
|
||||
</div>
|
||||
{#each $lessons as lesson, index}
|
||||
{#if lessonMapping[lesson.id]}
|
||||
@@ -117,7 +118,7 @@
|
||||
{/if}
|
||||
{/each}
|
||||
<div class="box flex items-center {borderleftGrey}">
|
||||
<p class="dark:text-white w-20 text-center">Total</p>
|
||||
<p class="dark:text-white w-20 text-center">{$t('course.navItem.marks.total')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -163,7 +164,9 @@
|
||||
{:else}
|
||||
<Box>
|
||||
<AudioConsoleIcon size={32} class="carbon-icon w-80" />
|
||||
<h3 class="text-3xl text-gray-500 dark:text-white">No Student Added</h3>
|
||||
<h3 class="text-3xl text-gray-500 dark:text-white">
|
||||
{$t('course.navItem.marks.no_student')}
|
||||
</h3>
|
||||
</Box>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import type { GroupPerson } from '$lib/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations.js';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
@@ -102,7 +103,7 @@
|
||||
}
|
||||
|
||||
function copyToClipboard(studentAssignedId: string | null) {
|
||||
snackbar.success(`Copied to clipboard`);
|
||||
snackbar.success(`snackbar.people.success.copied`);
|
||||
|
||||
if (studentAssignedId) {
|
||||
copy(studentAssignedId);
|
||||
@@ -125,10 +126,14 @@
|
||||
/>
|
||||
|
||||
<CourseContainer bind:courseId={data.courseId}>
|
||||
<PageNav title="People" disableSticky={true}>
|
||||
<PageNav title={$t('course.navItem.people.title')} disableSticky={true}>
|
||||
<slot:fragment slot="widget">
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<PrimaryButton className="mr-2" label="Add" onClick={handleClick} />
|
||||
<PrimaryButton
|
||||
className="mr-2"
|
||||
label={$t('course.navItem.people.add')}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
</RoleBasedSecurity>
|
||||
</slot:fragment>
|
||||
</PageNav>
|
||||
@@ -140,14 +145,14 @@
|
||||
<div class="max-w-[320px]">
|
||||
<Search
|
||||
class="dark:text-slate-950 border-0 bg-zinc-100 w-full"
|
||||
placeholder="Search people"
|
||||
placeholder={$t('course.navItem.people.search')}
|
||||
bind:value={searchValue}
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<Select
|
||||
bind:value={filterBy}
|
||||
options={ROLES}
|
||||
options={ROLES.map((role) => ({ label: $t(role.label), value: role.value }))}
|
||||
className="dark:text-black mt-3 max-w-[80px]"
|
||||
/>
|
||||
<!-- <select bind:value={filterBy} class="mt-3">
|
||||
@@ -167,13 +172,13 @@
|
||||
>
|
||||
<StructuredListRow head class="mx-7">
|
||||
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
|
||||
>Name</StructuredListCell
|
||||
>{$t('course.navItem.people.name')}</StructuredListCell
|
||||
>
|
||||
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
|
||||
>Role</StructuredListCell
|
||||
>{$t('course.navItem.people.role')}</StructuredListCell
|
||||
>
|
||||
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
|
||||
>Action</StructuredListCell
|
||||
>{$t('course.navItem.people.action')}</StructuredListCell
|
||||
>
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<p class="dark:text-white hidden lg:block text-lg w-20" />
|
||||
@@ -212,7 +217,7 @@
|
||||
/>
|
||||
</RoleBasedSecurity>
|
||||
{#if person.profile_id == $profile.id}
|
||||
<ComingSoon label="You" />
|
||||
<ComingSoon label={$t('course.navItem.people.you')} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -232,11 +237,14 @@
|
||||
</a>
|
||||
<div class="flex items-center justify-between">
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<CopyButton text={getEmail(person)} feedback="Copied Email to clipboard" />
|
||||
<CopyButton
|
||||
text={getEmail(person)}
|
||||
feedback={$t('course.navItem.people.feedback')}
|
||||
/>
|
||||
</RoleBasedSecurity>
|
||||
|
||||
<TextChip
|
||||
value="Pending"
|
||||
value={$t('course.navItem.people.pending')}
|
||||
className="text-xs bg-yellow-200 text-yellow-700 h-fit"
|
||||
size="sm"
|
||||
/>
|
||||
@@ -248,7 +256,7 @@
|
||||
<!-- second column -->
|
||||
<StructuredListCell class="w-1/4 px-3">
|
||||
<p class=" dark:text-white font-normal text-center text-base w-1/4">
|
||||
{ROLE_LABEL[person.role_id]}
|
||||
{$t(ROLE_LABEL[person.role_id])}
|
||||
</p>
|
||||
</StructuredListCell>
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
import PageNav from '$lib/components/PageNav/index.svelte';
|
||||
import PageBody from '$lib/components/PageBody/index.svelte';
|
||||
import Settings from '$lib/components/Course/components/Settings/index.svelte';
|
||||
import { t } from '../../../../lib/utils/functions/translations';
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<CourseContainer bind:courseId={data.courseId}>
|
||||
<PageNav title="Settings" />
|
||||
<PageNav title={$t('course.navItem.settings.heading')} />
|
||||
<PageBody>
|
||||
<Settings />
|
||||
</PageBody>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
} from '$lib/utils/services/notification/notification';
|
||||
import { currentOrg, currentOrgDomain } from '$lib/utils/store/org';
|
||||
import { browser } from '$app/environment';
|
||||
import { t } from '$lib/utils/functions/translations.js';
|
||||
|
||||
type items = {
|
||||
id: number;
|
||||
@@ -62,27 +63,27 @@
|
||||
let openExercise = false;
|
||||
|
||||
const submissionStatus: { [key: number]: string } = {
|
||||
1: 'Submitted',
|
||||
2: 'In Progress',
|
||||
3: 'Graded'
|
||||
1: $t('course.navItem.submissions.submission_status.1'),
|
||||
2: $t('course.navItem.submissions.submission_status.2'),
|
||||
3: $t('course.navItem.submissions.submission_status.3')
|
||||
};
|
||||
|
||||
let sections: sectionType[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Submitted',
|
||||
title: $t('course.navItem.submissions.submission_status.1'),
|
||||
value: 0,
|
||||
items: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'In Progress',
|
||||
title: $t('course.navItem.submissions.submission_status.2'),
|
||||
value: 0,
|
||||
items: []
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Graded',
|
||||
title: $t('course.navItem.submissions.submission_status.3'),
|
||||
value: 10,
|
||||
items: []
|
||||
}
|
||||
@@ -251,7 +252,7 @@
|
||||
total: totalPoints
|
||||
}).then((res) => console.log('Updated submission', res));
|
||||
|
||||
snackbar.success('Grading saved and student notified');
|
||||
snackbar.success('snackbar.submissions.success.grading');
|
||||
|
||||
await Promise.all(updates);
|
||||
}
|
||||
@@ -337,7 +338,7 @@
|
||||
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<CourseContainer bind:courseId={data.courseId}>
|
||||
<PageNav title="Submitted Exercises" />
|
||||
<PageNav title={$t('course.navItem.submissions.title')} />
|
||||
|
||||
<PageBody width="w-full max-w-6xl md:w-11/12 overflow-x-auto">
|
||||
<div class="flex items-center w-full">
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
loading = false;
|
||||
if (addedMember.error) {
|
||||
console.error('Error adding student to group', data.groupId, addedMember.error);
|
||||
snackbar.error('Joining failed, please contact your admin');
|
||||
snackbar.error('snackbar.invite.failed_join');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { courses, courseMetaDeta } from '$lib/components/Courses/store';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { getGreeting } from '$lib/utils/functions/date';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
let hasFetched = false;
|
||||
let progressPercentage = 0;
|
||||
@@ -52,7 +53,7 @@
|
||||
<section class="max-w-6xl gap-5 mx-auto">
|
||||
<div class="m-5">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold mb-3">
|
||||
{getGreeting()}
|
||||
{$t(getGreeting())}
|
||||
{$profile.fullname}!
|
||||
</h1>
|
||||
<div
|
||||
@@ -60,11 +61,10 @@
|
||||
>
|
||||
<span>
|
||||
<p class="w-full md:w-[75%] lg:w-[80%] text-white text-xs lg:text-xl font-normal mb-5">
|
||||
Today is another day to bring you closer to your learning goals. Don’t give up, the more
|
||||
you learn the better you get.
|
||||
{$t('dashboard.lms_dashboard_hero')}
|
||||
</p>
|
||||
<PrimaryButton
|
||||
label="Don't give up"
|
||||
label={$t('dashboard.dont')}
|
||||
variant={VARIANTS.CONTAINED_WHITE}
|
||||
onClick={() => goto('/lms/mylearning')}
|
||||
/>
|
||||
@@ -80,7 +80,9 @@
|
||||
<Learning />
|
||||
</div>
|
||||
<div class="mt-10 xl:mt-2 w-full lg:w-[50%]">
|
||||
<p class="text-base font-semibold text-[#040F2D] pb-3 dark:text-white">Your Progress</p>
|
||||
<p class="text-base font-semibold text-[#040F2D] pb-3 dark:text-white">
|
||||
{$t('dashboard.your_progress')}
|
||||
</p>
|
||||
<div
|
||||
class="flex items-center justify-center border border-[#EAEAEA] dark:bg-neutral-800 gap-2 rounded h-fit lg:h-[40vh] lg:overflow-y-auto p-3"
|
||||
>
|
||||
@@ -92,14 +94,17 @@
|
||||
</div>
|
||||
<span class="text-center xl:text-start">
|
||||
<p class="text-base font-semibold py-2 text-[#040F2D] dark:text-white">
|
||||
Your Progress
|
||||
{$t('dashboard.your_progress')}
|
||||
</p>
|
||||
{#if totalLessons > 0}
|
||||
<p class="text-xs font-normal text-[#656565] dark:text-white">
|
||||
{totalCompleted}/{totalLessons} lessons completed
|
||||
{totalCompleted}/{totalLessons}
|
||||
{$t('dashboard.lessons_completed')}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-xs font-normal text-[#656565] dark:text-white">No courses started</p>
|
||||
<p class="text-xs font-normal text-[#656565] dark:text-white">
|
||||
{$t('dashboard.No_courses_started')}
|
||||
</p>
|
||||
{/if}
|
||||
</span>
|
||||
<h1
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import Community from '$lib/components/Org/Community/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
function askCommunity() {
|
||||
goto(`/lms/community/ask`);
|
||||
@@ -15,8 +16,8 @@
|
||||
<section class="w-full max-w-4xl mx-auto">
|
||||
<div class="py-10 px-5">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">Community</h1>
|
||||
<PrimaryButton label="Ask Community" onClick={askCommunity} />
|
||||
<h1 class="dark:text-white text-3xl font-bold">{$t('community.title')}</h1>
|
||||
<PrimaryButton label={$t('community.ask_button')} onClick={askCommunity} />
|
||||
</div>
|
||||
|
||||
<Community isLMS={true} />
|
||||
|
||||
@@ -178,11 +178,11 @@
|
||||
|
||||
if (error) {
|
||||
console.error('Error: commenting', error);
|
||||
snackbar.error('Error - Please try again later');
|
||||
snackbar.error('snackbar.community.error.try_again');
|
||||
} else {
|
||||
console.log('Success: commenting', data);
|
||||
|
||||
snackbar.success('Comment Submitted!');
|
||||
snackbar.success('snackbar.community.success.comment_submitted');
|
||||
|
||||
// Add to comment
|
||||
const _c = data?.[0];
|
||||
@@ -230,7 +230,7 @@
|
||||
const { error } = await supabase.from(table).update({ votes }).match({ id: matchId });
|
||||
if (error) {
|
||||
console.error('Error: upvoteQuestion', error);
|
||||
snackbar.error('Error - Please try again later');
|
||||
snackbar.error('snackbar.community.error.try_again');
|
||||
} else {
|
||||
if (isQuestion) {
|
||||
voted.question = true;
|
||||
@@ -263,13 +263,14 @@
|
||||
const { error } = await supabase
|
||||
.from('community_question')
|
||||
.update({
|
||||
...editContent,
|
||||
title: editContent.title,
|
||||
body: editContent.body,
|
||||
course_id: editContent.courseId
|
||||
})
|
||||
.match({ id: question.id });
|
||||
if (error) {
|
||||
console.error('Error: handleQuestionEdit', error);
|
||||
snackbar.error('Error - Please try again later');
|
||||
snackbar.error('snackbar.community.error.try_again');
|
||||
} else {
|
||||
question.title = editContent.title;
|
||||
question.body = editContent.body;
|
||||
@@ -298,11 +299,11 @@
|
||||
deleteComment.isDeleting = false;
|
||||
|
||||
if (error) {
|
||||
snackbar.error('Error deleting comments');
|
||||
snackbar.error('snackbar.community.error.deleting_comments');
|
||||
console.log('Error deleting comments', error);
|
||||
return;
|
||||
}
|
||||
snackbar.success('Deleted successfully');
|
||||
snackbar.success('snackbar.community.success.success_delete');
|
||||
|
||||
question.comments = question.comments.filter((c) => c.id !== deleteComment.commentId);
|
||||
deleteComment.shouldDelete = false;
|
||||
@@ -319,7 +320,7 @@
|
||||
.match({ question_id: deleteQuestion.questionId });
|
||||
|
||||
if (commentDeleteError) {
|
||||
snackbar.error('Error deleting comments');
|
||||
snackbar.error('snackbar.community.error.deleting_comments');
|
||||
console.log('Error deleting comments', commentDeleteError);
|
||||
|
||||
deleteQuestion.isDeleting = false;
|
||||
@@ -332,12 +333,12 @@
|
||||
.match({ id: deleteQuestion.questionId });
|
||||
|
||||
if (questionDeleteError) {
|
||||
snackbar.error('Error deleting question');
|
||||
snackbar.error('snackbar.community.error.deleting_question');
|
||||
console.log('Error deleting question', questionDeleteError);
|
||||
return;
|
||||
}
|
||||
|
||||
snackbar.success('Deleted successfully');
|
||||
snackbar.success('snackbar.community.success.success_delete');
|
||||
goto(`/lms/community`);
|
||||
deleteQuestion.isDeleting = false;
|
||||
}
|
||||
@@ -394,7 +395,7 @@
|
||||
errorMessage={errors.title}
|
||||
/>
|
||||
<Dropdown
|
||||
class="w-[25%] h-fit"
|
||||
class="w-[25%] h-full"
|
||||
size="xl"
|
||||
label="Select Course"
|
||||
items={fetchedCourses.map((course) => ({ id: course.id, text: course.title }))}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { fetchCourses } from '$lib/components/Courses/api';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
let errors: {
|
||||
title?: string;
|
||||
@@ -57,7 +58,7 @@
|
||||
} else {
|
||||
const slug = question[0]?.slug;
|
||||
console.log('Success: asking question in lms', question, slug);
|
||||
snackbar.success('Question Submitted!');
|
||||
snackbar.success('snackbar.community.success.question_submitted');
|
||||
|
||||
goto(`/lms/community/${slug}`);
|
||||
}
|
||||
@@ -77,25 +78,26 @@
|
||||
<section class="w-full max-w-3xl mx-auto">
|
||||
<div class="p-5">
|
||||
<a class="text-gray-500 dark:text-white text-md flex items-center" href={`/lms/community`}>
|
||||
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" /> Go Back
|
||||
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" />
|
||||
{$t('community.ask.go_back')}
|
||||
</a>
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="dark:text-white text-3xl font-bold">Ask the community</h1>
|
||||
<PrimaryButton label="Publish" onClick={handleSave} />
|
||||
<h1 class="dark:text-white text-3xl font-bold">{$t('community.ask.ask_the')}</h1>
|
||||
<PrimaryButton label={$t('community.ask.publish')} onClick={handleSave} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 p-2 flex gap-x-5 justify-between">
|
||||
<TextField
|
||||
bind:value={fields.title}
|
||||
placeholder="Title"
|
||||
placeholder={$t('community.ask.title')}
|
||||
errorMessage={errors.title}
|
||||
className="w-[75%]"
|
||||
/>
|
||||
<Dropdown
|
||||
class="w-[25%]"
|
||||
class="w-[25%] h-full"
|
||||
size="xl"
|
||||
label="Select Course"
|
||||
label={$t('community.ask.select_course')}
|
||||
invalid={!!errors.courseId}
|
||||
invalidText={errors.courseId}
|
||||
items={fetchedCourses.map((course) => ({ id: course.id, text: course.title }))}
|
||||
@@ -105,7 +107,7 @@
|
||||
|
||||
<TextEditor
|
||||
bind:value={fields.body}
|
||||
placeholder="Ask the community any question you might have"
|
||||
placeholder={$t('community.ask.ask_community')}
|
||||
onChange={(html) => (fields.body = html)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -8,29 +8,30 @@
|
||||
import { fetchLMSExercises } from '$lib/utils/services/lms/exercises';
|
||||
import type { LMSExercise } from '$lib/utils/services/lms/exercises';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
const defaultSections = [
|
||||
{
|
||||
id: 0,
|
||||
title: 'Not Submitted',
|
||||
title: $t('exercises.not_submitted'),
|
||||
items: [],
|
||||
className: 'text-[#E35353] bg-[#FDDFE4]'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: 'Submitted',
|
||||
title: $t('exercises.submitted'),
|
||||
items: [],
|
||||
className: 'text-orange-700 bg-orange-200'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Grading In Progress',
|
||||
title: $t('exercises.in_progress'),
|
||||
items: [],
|
||||
className: 'text-yellow-700 bg-yellow-200'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Graded',
|
||||
title: $t('exercises.graded'),
|
||||
value: 0,
|
||||
items: [],
|
||||
className: 'text-green-700 bg-green-200'
|
||||
@@ -105,7 +106,7 @@
|
||||
console.log('error', error);
|
||||
|
||||
if (error) {
|
||||
snackbar.error('Fetching exercises failed, please reload the page');
|
||||
snackbar.error('snackbar.exercise.error_fetching');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,7 +124,7 @@
|
||||
<section class="w-full max-w-6xl mx-auto">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">Exercises</h1>
|
||||
<h1 class="dark:text-white text-3xl font-bold">{$t('exercises.heading')}</h1>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -156,7 +157,7 @@
|
||||
href={item.lessonURL}
|
||||
>
|
||||
<p class="dark:text-white text-grey text-sm">
|
||||
Lesson: <span class="italic">{item.lessonTitle}</span>
|
||||
{$t('exercises.lesson')} <span class="italic">{item.lessonTitle}</span>
|
||||
</p>
|
||||
</a>
|
||||
<p class="dark:text-white text-gray-500 text-xs">
|
||||
|
||||
@@ -8,14 +8,15 @@
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { courses, courseMetaDeta, coursesComplete } from '$lib/components/Courses/store';
|
||||
import { browser } from '$app/environment';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: 'In progress',
|
||||
label: $t('my_learning.progress'),
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: 'Complete',
|
||||
label: $t('my_learning.complete'),
|
||||
value: 2
|
||||
}
|
||||
];
|
||||
@@ -55,23 +56,23 @@
|
||||
<section class="max-w-6xl mx-auto">
|
||||
<div class="m-2 md:m-5">
|
||||
<div role="searchbox" class=" bg-gray-100 w-full md:w-[60%] lg:w-[30%]">
|
||||
<Search placeholder="Search courses" class="dark:text-black" />
|
||||
<Search placeholder={$t('my_learning.search')} class="dark:text-black" />
|
||||
</div>
|
||||
<h1 class="text-3xl font-semibold my-4">My Learning</h1>
|
||||
<h1 class="text-3xl font-semibold my-4">{$t('my_learning.heading')}</h1>
|
||||
<Tabs {tabs} {currentTab} {onChange}>
|
||||
<slot:fragment slot="content">
|
||||
<TabContent value={tabs[0].value} index={currentTab}>
|
||||
<Courses
|
||||
courses={$courses}
|
||||
emptyTitle="No Course In progress"
|
||||
emptyDescription="Any course that you start will be displayed here"
|
||||
emptyTitle={$t('my_learning.not_in_progress')}
|
||||
emptyDescription={$t('my_learning.any_progress')}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={tabs[1].value} index={currentTab}>
|
||||
<Courses
|
||||
courses={$coursesComplete}
|
||||
emptyTitle="No Course Completed"
|
||||
emptyDescription="Any course that you complete will be displayed here"
|
||||
emptyTitle={$t('my_learning.not_completed')}
|
||||
emptyDescription={$t('my_learning.any_course')}
|
||||
/>
|
||||
</TabContent>
|
||||
</slot:fragment>
|
||||
|
||||
@@ -3,18 +3,20 @@
|
||||
import { Tabs, Tab, TabContent } from 'carbon-components-svelte';
|
||||
import Profile from '$lib/components/LMS/components/Profile.svelte';
|
||||
import Integrations from '$lib/components/Org/Settings/Integrations.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
let selected = 0;
|
||||
const tabs = [
|
||||
let tabs = [];
|
||||
$: tabs = [
|
||||
{
|
||||
key: 0,
|
||||
label: 'Profile',
|
||||
label: $t('settings.tabs.1'),
|
||||
tabKey: '',
|
||||
href: $page.url.pathname
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
label: 'Integrations',
|
||||
label: $t('settings.tabs.4'),
|
||||
tabKey: 'integrations',
|
||||
href: `${$page.url.pathname}?tab=integrations`,
|
||||
disabled: false
|
||||
@@ -25,7 +27,7 @@
|
||||
<section class="w-full max-w-6xl mx-auto">
|
||||
<div class="py-10 px-5">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">Settings</h1>
|
||||
<h1 class="dark:text-white text-3xl font-bold">{$t('settings.heading')}</h1>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import AuthUI from '$lib/components/AuthUI/index.svelte';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
|
||||
let formRef: HTMLFormElement;
|
||||
@@ -60,9 +61,9 @@
|
||||
|
||||
<AuthUI {supabase} isLogin={true} {handleSubmit} isLoading={loading} bind:formRef>
|
||||
<div class="mt-4 w-full">
|
||||
<p class="dark:text-white text-lg font-semibold mb-6">Welcome back</p>
|
||||
<p class="dark:text-white text-lg font-semibold mb-6">{$t('login.welcome')}</p>
|
||||
<TextField
|
||||
label="Your email"
|
||||
label={$t('login.email')}
|
||||
bind:value={fields.email}
|
||||
type="email"
|
||||
autoFocus={true}
|
||||
@@ -73,7 +74,7 @@
|
||||
errorMessage={errors.email}
|
||||
/>
|
||||
<TextField
|
||||
label="Your password"
|
||||
label={$t('login.password')}
|
||||
bind:value={fields.password}
|
||||
type="password"
|
||||
placeholder="************"
|
||||
@@ -86,14 +87,14 @@
|
||||
<p class="text-sm text-red-500">{submitError}</p>
|
||||
{/if}
|
||||
<div class="w-full text-right">
|
||||
<a class="text-md text-primary-700" href="/forgot"> Forgot password? </a>
|
||||
<a class="text-md text-primary-700" href="/forgot"> {$t('login.forgot')} </a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-4 w-full flex justify-end items-center">
|
||||
<!-- <a href="/login" class="text-primary-700 text-sm">Create an account</a> -->
|
||||
<PrimaryButton
|
||||
label="Log In"
|
||||
label={$t('login.login')}
|
||||
type="submit"
|
||||
className="sm:w-full w-full"
|
||||
isDisabled={loading}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { PUBLIC_IP_REGISTRY_KEY } from '$env/static/public';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Dropdown } from 'carbon-components-svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import UserProfileIcon from '$lib/components/Icons/UserProfileIcon.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
@@ -15,6 +16,7 @@
|
||||
triggerSendEmail,
|
||||
NOTIFICATION_NAME
|
||||
} from '$lib/utils/services/notification/notification';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
interface OnboardingField {
|
||||
fullname?: string;
|
||||
@@ -22,6 +24,7 @@
|
||||
siteName?: string;
|
||||
goal?: string;
|
||||
source?: string;
|
||||
locale?: string;
|
||||
metadata?: {};
|
||||
}
|
||||
|
||||
@@ -34,52 +37,70 @@
|
||||
let fields: OnboardingField = {
|
||||
fullname: '',
|
||||
orgName: '',
|
||||
siteName: ''
|
||||
siteName: '',
|
||||
locale: ''
|
||||
};
|
||||
let errors: OnboardingField = {};
|
||||
let progress = 50;
|
||||
let step = 1;
|
||||
let loading = false;
|
||||
let isSiteNameTouched = false;
|
||||
let selectedId = 'en';
|
||||
|
||||
let dropdownItems = [
|
||||
{ id: 'en', text: 'English' },
|
||||
{ id: 'hi', text: 'Hindi' },
|
||||
{ id: 'fr', text: 'French' },
|
||||
{ id: 'pt', text: 'Portuguese' },
|
||||
{ id: 'de', text: 'German' },
|
||||
{ id: 'vi', text: 'Vietnamese' },
|
||||
{ id: 'ru', text: 'Russian' },
|
||||
{ id: 'es', text: 'Spanish' }
|
||||
];
|
||||
|
||||
const educatorGoals: Goal[] = [
|
||||
{
|
||||
label: 'Sell courses online',
|
||||
label: $t('onboarding.sell'),
|
||||
value: 'sell-online'
|
||||
},
|
||||
{
|
||||
label: 'Teach my current students online',
|
||||
label: $t('onboarding.teach'),
|
||||
value: 'teach-online'
|
||||
},
|
||||
{
|
||||
label: 'On another platform, expanding here',
|
||||
label: $t('onboarding.expanding'),
|
||||
value: 'expanding-platform'
|
||||
},
|
||||
{
|
||||
label: 'Just here to explore',
|
||||
label: $t('onboarding.explore'),
|
||||
value: 'explore'
|
||||
}
|
||||
];
|
||||
|
||||
const sources = [
|
||||
{
|
||||
label: 'Articles',
|
||||
label: $t('onboarding.articles'),
|
||||
value: 'articles'
|
||||
},
|
||||
{
|
||||
label: 'Search engine',
|
||||
label: $t('onboarding.search'),
|
||||
value: 'search-engine'
|
||||
},
|
||||
{
|
||||
label: 'Social media',
|
||||
label: $t('onboarding.social'),
|
||||
value: 'social-media'
|
||||
},
|
||||
{
|
||||
label: 'Friends and family',
|
||||
label: $t('onboarding.friends'),
|
||||
value: 'friends-family'
|
||||
}
|
||||
];
|
||||
|
||||
function handleSelect(event) {
|
||||
const newSelectedId = event.detail.selectedId;
|
||||
fields.locale = newSelectedId;
|
||||
}
|
||||
|
||||
async function setMetaData() {
|
||||
if (!PUBLIC_IP_REGISTRY_KEY) return;
|
||||
|
||||
@@ -146,8 +167,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: fields.siteName });
|
||||
@@ -225,7 +245,7 @@
|
||||
<div id="role-question" class="flex items-start flex-col mb-6">
|
||||
<!-- Full name -->
|
||||
<TextField
|
||||
label="Full Name"
|
||||
label={$t('onboarding.fullname')}
|
||||
bind:value={fields.fullname}
|
||||
name="fullname"
|
||||
type="text"
|
||||
@@ -238,7 +258,7 @@
|
||||
|
||||
<!-- Org name -->
|
||||
<TextField
|
||||
label="Name of Organization"
|
||||
label={$t('onboarding.name')}
|
||||
bind:value={fields.orgName}
|
||||
name="orgname"
|
||||
type="text"
|
||||
@@ -251,7 +271,7 @@
|
||||
|
||||
<!-- Org Site Name -->
|
||||
<TextField
|
||||
label="Organisation Site name"
|
||||
label={$t('onboarding.organisation_sitename')}
|
||||
helperMessage={`https://${fields.siteName || ''}.classroomio.com`}
|
||||
bind:value={fields.siteName}
|
||||
name="sitename"
|
||||
@@ -266,6 +286,7 @@
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
|
|
||||
{:else}
|
||||
<!-- Goal/Source Question -->
|
||||
<div id="goal-question" class="flex items-center flex-col mb-6">
|
||||
@@ -273,7 +294,7 @@
|
||||
<!-- Goal Question -->
|
||||
<div class="w-full flex items-start flex-col justify-between mb-10">
|
||||
<label for="text-field" class="dark:text-white m-0 text-lg font-normal mb-3">
|
||||
What brings you to ClassroomIO?
|
||||
{$t('onboarding.what_brings')}
|
||||
</label>
|
||||
|
||||
<!-- Loop through Goals -->
|
||||
@@ -300,7 +321,7 @@
|
||||
<!-- Source Question -->
|
||||
<div class="w-full flex items-start flex-col justify-between">
|
||||
<label for="text-field" class="dark:text-white m-0 text-lg font-normal mb-3">
|
||||
How did you hear about us?
|
||||
{$t('onboarding.how')}
|
||||
</label>
|
||||
|
||||
<!-- Loop through Goals -->
|
||||
@@ -323,6 +344,17 @@
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Language Picker -->
|
||||
<div class="mt-10">
|
||||
<span>{$t('content.toggle_label')}: </span>
|
||||
<Dropdown
|
||||
items={dropdownItems}
|
||||
{selectedId}
|
||||
on:select={handleSelect}
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -340,14 +372,14 @@
|
||||
<div class="flex">
|
||||
{#if step > 1}
|
||||
<PrimaryButton
|
||||
label="Back"
|
||||
label={$t('onboarding.back')}
|
||||
variant={VARIANTS.NONE}
|
||||
className="py-3 px-6 mr-2 text-primary-700"
|
||||
onClick={() => (step = step - 1)}
|
||||
/>
|
||||
{/if}
|
||||
<PrimaryButton
|
||||
label="Continue"
|
||||
label={$t('onboarding.continue')}
|
||||
variant={VARIANTS.CONTAINED}
|
||||
type="button"
|
||||
className="px-5 py-3"
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
</script>
|
||||
|
||||
<AddOrgModal />
|
||||
|
||||
<VerifyEmailModal />
|
||||
|
||||
<div class="org-root w-full flex items-center justify-between">
|
||||
{#if !isQuizPage($page.url?.pathname)}
|
||||
<OrgSideBar />
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
// import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { onMount } from 'svelte';
|
||||
import { Add } from 'carbon-icons-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import UnlockedIcon from 'carbon-icons-svelte/lib/Unlocked.svelte';
|
||||
import LockedIcon from 'carbon-icons-svelte/lib/Locked.svelte';
|
||||
// import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import { InlineCalendar, Datepicker } from 'svelte-calendar';
|
||||
// import { supabase } from '$lib/utils/functions/supabase';
|
||||
// import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { formatUserUpcomingData, formatDate } from '$lib/utils/functions/routes/dashboard';
|
||||
import { user } from '$lib/utils/store/user';
|
||||
import { fetchUserUpcomingData } from '$lib/utils/services/dashboard';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import type { UserLessonDataType, LessonsByMonthIndexType } from '$lib/utils/types';
|
||||
import WelcomeModal from '$lib/components/WelcomeModal/WelcomeModal.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { Add } from 'carbon-icons-svelte';
|
||||
import { isOrgAdmin, currentOrgPath, currentOrg, userUpcomingData } from '$lib/utils/store/org';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
|
||||
import { getGreeting } from '$lib/utils/functions/date';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { t } from '$lib/utils/functions/translations.js';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -187,7 +188,7 @@
|
||||
<div class="py-10 px-5 w-full max-w-5xl md:mx-auto">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold mb-3">
|
||||
{getGreeting()}
|
||||
{$t(getGreeting())}
|
||||
{$profile.fullname}!
|
||||
</h1>
|
||||
<div class="flex items-center">
|
||||
@@ -200,7 +201,7 @@
|
||||
{#if $isMobile}
|
||||
<Add size={24} />
|
||||
{:else}
|
||||
Create Course
|
||||
{$t('dashboard.create_course')}
|
||||
{/if}
|
||||
</PrimaryButton>
|
||||
|
||||
@@ -213,11 +214,10 @@
|
||||
>
|
||||
<span>
|
||||
<p class="w-full md:w-[75%] lg:w-[80%] text-white text-xs lg:text-xl font-normal mb-5">
|
||||
Thank you for what you do ❤️. You are changing lives one classroom at a time and thanks to
|
||||
you, the world is a better place. 😊
|
||||
{$t('dashboard.hero_content')}
|
||||
</p>
|
||||
<PrimaryButton
|
||||
label="Keep Building 🚀"
|
||||
label={$t('dashboard.hero_button')}
|
||||
variant={VARIANTS.CONTAINED_WHITE}
|
||||
onClick={() => goto(`${$currentOrgPath}/courses`)}
|
||||
/>
|
||||
@@ -243,7 +243,7 @@
|
||||
<div class="w-full">
|
||||
<!-- Your Schedule -->
|
||||
<div class="w-full xl:w-auto container">
|
||||
<p class="dark:text-white font-bold mt-7 mb-4">Your Schedule</p>
|
||||
<p class="dark:text-white font-bold mt-7 mb-4">{$t('dashboard.your_schedule')}</p>
|
||||
<div
|
||||
class="rounded border border-gray-200 dark:border-neutral-600 gap-3 flex flex-col md:flex-row items-start px-2 md:px-5 py-5 w-full"
|
||||
>
|
||||
@@ -305,16 +305,16 @@
|
||||
href={!!lessonData.call_url ? lessonData.call_url : undefined}
|
||||
target="_blank"
|
||||
title={!!lessonData.call_url
|
||||
? 'Click to join the call'
|
||||
: 'No call link was added for this lesson. Ask your trainer'}
|
||||
? $t('dashboard.click_to_join')
|
||||
: $t('dashboard.no_call_link')}
|
||||
>
|
||||
Join call
|
||||
{$t('dashboard.join_call')}
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="dark:text-white flex items-center justify-center w-full no-data">
|
||||
No lesson on this day
|
||||
{$t('dashboard.no_schedule')}
|
||||
</p>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import Audience from '$lib/components/Org/Audience/index.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { orgAudience, currentOrgPlan, currentOrgMaxAudience } from '$lib/utils/store/org';
|
||||
import { PLAN } from 'shared-constants/src/plans/constants';
|
||||
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||
@@ -22,14 +23,19 @@
|
||||
<div class="py-10 px-5">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<div class="flex items-end">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold m-0">Audience</h1>
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold m-0">{$t('audience.title')}</h1>
|
||||
{#if $currentOrgPlan?.plan_name !== PLAN.ENTERPRISE}
|
||||
<span class="ml-2">
|
||||
({$orgAudience.length} / {$currentOrgMaxAudience})
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<PrimaryButton label="Export" onClick={exportAudience} isDisabled={isLoading} {isLoading} />
|
||||
<PrimaryButton
|
||||
label={$t('audience.export')}
|
||||
onClick={exportAudience}
|
||||
isDisabled={isLoading}
|
||||
{isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if $orgAudience.length >= $currentOrgMaxAudience}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { currentOrgPath } from '$lib/utils/store/org';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import Add from 'carbon-icons-svelte/lib/Add.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
function askCommunity() {
|
||||
goto(`${$currentOrgPath}/community/ask`);
|
||||
@@ -19,14 +20,14 @@
|
||||
<section class="w-full md:max-w-4xl md:mx-auto">
|
||||
<div class="w-full py-10 px-5">
|
||||
<div class="flex items-center justify-between mb-10 w-full">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">Community</h1>
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">{$t('community.title')}</h1>
|
||||
{#if $isMobile}
|
||||
<PrimaryButton onClick={askCommunity}>
|
||||
<Add size={24} />
|
||||
</PrimaryButton>
|
||||
{:else}
|
||||
<PrimaryButton
|
||||
label="Ask Community"
|
||||
label={$t('community.ask_button')}
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
onClick={askCommunity}
|
||||
/>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { browser } from '$app/environment';
|
||||
import { fetchCourses } from '$lib/components/Courses/api.js';
|
||||
import { t } from '$lib/utils/functions/translations.js';
|
||||
|
||||
export let data;
|
||||
const { slug } = data;
|
||||
@@ -177,11 +178,11 @@
|
||||
|
||||
if (error) {
|
||||
console.error('Error: commenting', error);
|
||||
snackbar.error('Error - Please try again later');
|
||||
snackbar.error('snackbar.community.error.try_again');
|
||||
} else {
|
||||
console.log('Success: commenting', data);
|
||||
|
||||
snackbar.success('Comment Submitted!');
|
||||
snackbar.success('snackbar.community.success.comment_submitted');
|
||||
|
||||
// Add to comment
|
||||
const _c = data?.[0];
|
||||
@@ -229,7 +230,7 @@
|
||||
const { error } = await supabase.from(table).update({ votes }).match({ id: matchId });
|
||||
if (error) {
|
||||
console.error('Error: upvoteQuestion', error);
|
||||
snackbar.error('Error - Please try again later');
|
||||
snackbar.error('snackbar.community.error.try_again');
|
||||
} else {
|
||||
if (isQuestion) {
|
||||
voted.question = true;
|
||||
@@ -262,13 +263,14 @@
|
||||
const { error } = await supabase
|
||||
.from('community_question')
|
||||
.update({
|
||||
...editContent,
|
||||
title: editContent.title,
|
||||
body: editContent.body,
|
||||
course_id: editContent.courseId
|
||||
})
|
||||
.match({ id: question.id });
|
||||
if (error) {
|
||||
console.error('Error: handleQuestionEdit', error);
|
||||
snackbar.error('Error - Please try again later');
|
||||
snackbar.error('snackbar.community.error.try_again');
|
||||
} else {
|
||||
question.title = editContent.title;
|
||||
question.body = editContent.body;
|
||||
@@ -297,11 +299,11 @@
|
||||
deleteComment.isDeleting = false;
|
||||
|
||||
if (error) {
|
||||
snackbar.error('Error deleting comments');
|
||||
snackbar.error('snackbar.community.error.deleting_comments');
|
||||
console.log('Error deleting comments', error);
|
||||
return;
|
||||
}
|
||||
snackbar.success('Deleted successfully');
|
||||
snackbar.success('snackbar.community.success.success_delete');
|
||||
|
||||
question.comments = question.comments.filter((c) => c.id !== deleteComment.commentId);
|
||||
deleteComment.shouldDelete = false;
|
||||
@@ -318,7 +320,7 @@
|
||||
.match({ question_id: deleteQuestion.questionId });
|
||||
|
||||
if (commentDeleteError) {
|
||||
snackbar.error('Error deleting comments');
|
||||
snackbar.error('snackbar.community.error.deleting_comments');
|
||||
console.log('Error deleting comments', commentDeleteError);
|
||||
|
||||
deleteQuestion.isDeleting = false;
|
||||
@@ -331,12 +333,12 @@
|
||||
.match({ id: deleteQuestion.questionId });
|
||||
|
||||
if (questionDeleteError) {
|
||||
snackbar.error('Error deleting question');
|
||||
snackbar.error('snackbar.community.error.deleting_question');
|
||||
console.log('Error deleting question', questionDeleteError);
|
||||
return;
|
||||
}
|
||||
|
||||
snackbar.success('Deleted successfully');
|
||||
snackbar.success('snackbar.community.success.success_delete');
|
||||
goto(`${$currentOrgPath}/community`);
|
||||
deleteQuestion.isDeleting = false;
|
||||
}
|
||||
@@ -350,7 +352,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{question?.title || 'Question'}</title>
|
||||
<title>{question?.title || $t('community.ask.question')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<DeleteModal
|
||||
@@ -398,7 +400,7 @@
|
||||
<Dropdown
|
||||
class="w-[25%] h-full"
|
||||
size="xl"
|
||||
label="Select Course"
|
||||
label={$t('community.ask.select_course')}
|
||||
items={fetchedCourses.map((course) => ({ id: course.id, text: course.title }))}
|
||||
bind:selectedId={editContent.courseId}
|
||||
/>
|
||||
@@ -415,14 +417,14 @@
|
||||
|
||||
{#if question.author.id === $profile.id}
|
||||
<PrimaryButton
|
||||
label={isEditMode ? 'Save' : 'Edit'}
|
||||
label={isEditMode ? $t('community.ask.save') : $t('community.ask.edit')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={handleQuestionEdit}
|
||||
className="h-fit"
|
||||
/>
|
||||
{#if isEditMode}
|
||||
<PrimaryButton
|
||||
label="Cancel"
|
||||
label={$t('community.ask.cancel')}
|
||||
variant={VARIANTS.TEXT}
|
||||
onClick={() => (isEditMode = !isEditMode)}
|
||||
className="py-3 px-6 rounded-sm h-fit"
|
||||
@@ -461,7 +463,7 @@
|
||||
<div class="my-2">
|
||||
<TextEditor
|
||||
bind:value={editContent.body}
|
||||
placeholder="Give an answer"
|
||||
placeholder={$t('community.ask.give')}
|
||||
onChange={(html) => (editContent.body = html)}
|
||||
/>
|
||||
</div>
|
||||
@@ -522,13 +524,13 @@
|
||||
{#if !editorInstance}
|
||||
<TextEditor
|
||||
bind:value={comment}
|
||||
placeholder="Give an answer"
|
||||
placeholder={$t('community.ask.give')}
|
||||
onChange={(html) => (comment = html)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-end mr-2">
|
||||
<PrimaryButton label="Comment" onClick={submitComment} />
|
||||
<PrimaryButton label={$t('community.ask.comment')} onClick={submitComment} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { fetchCourses } from '$lib/components/Courses/api';
|
||||
import { t } from '$lib/utils/functions/translations.js';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -26,10 +27,12 @@
|
||||
courseId: ''
|
||||
};
|
||||
|
||||
let fetchedCourses = [];
|
||||
let fetchedCourses: any[] = [];
|
||||
|
||||
async function getCourses(userId: string | null, orgId: string) {
|
||||
const coursesResults = await fetchCourses(userId, orgId);
|
||||
if (!coursesResults) return;
|
||||
|
||||
fetchedCourses = coursesResults.allCourses;
|
||||
}
|
||||
|
||||
@@ -56,12 +59,12 @@
|
||||
|
||||
if (error) {
|
||||
console.error('Error: asking question', error);
|
||||
snackbar.error('Error - Please try again later');
|
||||
snackbar.error('snackbar.community.error.try_again');
|
||||
} else {
|
||||
const slug = question[0]?.slug;
|
||||
|
||||
console.log('Success: asking question', question, slug);
|
||||
snackbar.success('Question Submitted!');
|
||||
snackbar.success('snackbar.community.success.question_submitted');
|
||||
goto(`${$currentOrgPath}/community/${slug}`);
|
||||
}
|
||||
}
|
||||
@@ -83,25 +86,30 @@
|
||||
class="text-gray-500 dark:text-white text-md flex items-center"
|
||||
href={`${$currentOrgPath}/community`}
|
||||
>
|
||||
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" /> Go Back
|
||||
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" />
|
||||
{$t('community.ask.go_back')}
|
||||
</a>
|
||||
<div class="flex items-center gap-12 justify-between">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">Ask the community</h1>
|
||||
<PrimaryButton label="Publish" variant={VARIANTS.CONTAINED_DARK} onClick={handleSave} />
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">{$t('community.ask.ask_the')}</h1>
|
||||
<PrimaryButton
|
||||
label={$t('community.ask.publish')}
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
onClick={handleSave}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 p-2 flex gap-x-5 justify-between">
|
||||
<TextField
|
||||
bind:value={fields.title}
|
||||
placeholder="Title"
|
||||
placeholder={$t('community.ask.title')}
|
||||
errorMessage={errors.title}
|
||||
className="w-[75%]"
|
||||
/>
|
||||
<Dropdown
|
||||
class="w-[25%]"
|
||||
class="w-[25%] h-full"
|
||||
size="xl"
|
||||
label="Select Course"
|
||||
label={$t('community.ask.select_course')}
|
||||
invalid={!!errors.courseId}
|
||||
invalidText={errors.courseId}
|
||||
items={fetchedCourses.map((course) => ({ id: course.id, text: course.title }))}
|
||||
@@ -111,7 +119,7 @@
|
||||
<div class="px-2">
|
||||
<TextEditor
|
||||
bind:value={fields.body}
|
||||
placeholder="Ask the community any question you might have"
|
||||
placeholder={$t('community.ask.ask_community')}
|
||||
onChange={(html) => (fields.body = html)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { browser } from '$app/environment';
|
||||
import { t } from '$lib/utils/functions/translations.js';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
|
||||
export let data;
|
||||
@@ -86,14 +87,14 @@
|
||||
<section class="w-full md:max-w-6xl md:mx-auto">
|
||||
<div class="py-2 md:py-10 px-2 md:px-5">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">Courses</h1>
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">{$t('courses.heading')}</h1>
|
||||
{#if $isMobile}
|
||||
<PrimaryButton isDisabled={!$isOrgAdmin} onClick={() => ($createCourseModal.open = true)}>
|
||||
<Add size={24} />
|
||||
</PrimaryButton>
|
||||
{:else}
|
||||
<PrimaryButton
|
||||
label="Create Course"
|
||||
label={$t('courses.heading_button')}
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
isDisabled={!$isOrgAdmin}
|
||||
onClick={() => ($createCourseModal.open = true)}
|
||||
@@ -101,9 +102,9 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row-reverse mb-5">
|
||||
<div class="flex items-end justify-start">
|
||||
<div class="filter-containter flex items-end justify-start">
|
||||
<Search
|
||||
placeholder="Find course"
|
||||
placeholder={$t('courses.search_placeholder')}
|
||||
bind:value={searchValue}
|
||||
searchClass="mr-2"
|
||||
class=" bg-gray-100 dark:bg-neutral-800"
|
||||
@@ -112,9 +113,9 @@
|
||||
class="h-full"
|
||||
bind:selectedId
|
||||
items={[
|
||||
{ id: '0', text: 'Date Created' },
|
||||
{ id: '1', text: 'Published' },
|
||||
{ id: '2', text: 'Lessons' }
|
||||
{ id: '0', text: $t('courses.course_filter.date_created') },
|
||||
{ id: '1', text: $t('courses.course_filter.published') },
|
||||
{ id: '2', text: $t('courses.course_filter.lessons') }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
@@ -127,7 +128,7 @@
|
||||
</section>
|
||||
|
||||
<style>
|
||||
:global(.bx--dropdown) {
|
||||
.filter-containter :global(.bx--dropdown) {
|
||||
max-height: unset;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -146,10 +146,10 @@
|
||||
console.log('data', data);
|
||||
console.log('error', error);
|
||||
if (error) {
|
||||
snackbar.error("Something's not right - Please try later");
|
||||
snackbar.error('snackbar.course_settings.error.not_right');
|
||||
return;
|
||||
} else {
|
||||
snackbar.success('Saved Successfully');
|
||||
snackbar.success('snackbar.course_settings.success.saved');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import OrgSettings from '$lib/components/Org/Settings/index.svelte';
|
||||
import ViewSiteBtn from '$lib/components/Buttons/VisitOrgSite.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -10,7 +11,7 @@
|
||||
<section class="w-full md:max-w-4xl mx-auto">
|
||||
<div class="py-10 px-5">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">Settings</h1>
|
||||
<h1 class="dark:text-white text-3xl font-bold">{$t('settings.heading')}</h1>
|
||||
|
||||
<ViewSiteBtn />
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
import Chip from '$lib/components/Chip/Text.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import StepDoneIcon from '$lib/components/Icons/StepDoneIcon.svelte';
|
||||
import CheckmarkOutline from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
|
||||
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { goto } from '$app/navigation';
|
||||
import { snackbar } from '$lib/components/Snackbar/store.js';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
export let data;
|
||||
let setupList = data.setup || [];
|
||||
@@ -24,7 +24,7 @@
|
||||
};
|
||||
|
||||
const isCompleted = (id) => {
|
||||
return setupList?.find((c) => c.id === id).is_completed == true;
|
||||
return setupList?.find((c) => c.id === id)?.is_completed == true;
|
||||
};
|
||||
|
||||
const handleClick = (title) => {
|
||||
@@ -38,7 +38,7 @@
|
||||
const courseId = data?.courses[0].id;
|
||||
goto(`/courses/${courseId}/lessons`);
|
||||
} else {
|
||||
snackbar.info('You need to create a course');
|
||||
snackbar.info('setup.info_course');
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
const lessonId = data?.lessons[0].id;
|
||||
goto(`/courses/${courseId}/lessons/${lessonId}`);
|
||||
} else {
|
||||
snackbar.info('You Need to Create a lesson');
|
||||
snackbar.info('setup.info_lesson');
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
const courseId = data?.courses[0].id;
|
||||
goto(`/courses/${courseId}/settings`);
|
||||
} else {
|
||||
snackbar.info('You need to create a course');
|
||||
snackbar.info('setup.info_course');
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
<section class="w-full md:max-w-4xl mx-auto">
|
||||
<div class="py-2 md:py-10 px-2 md:px-5">
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">Get Started</h1>
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">{$t('setup.get_started')}</h1>
|
||||
<Chip
|
||||
value={`${completed}/${setupList.length}`}
|
||||
className="text-[10px] font-semibold px-3 !py-1"
|
||||
@@ -104,9 +104,11 @@
|
||||
className={`text-[10px] font-semibold !py-1 `}
|
||||
shape="rounded-full"
|
||||
/>
|
||||
<p class="font-medium text-lg">{list.title}</p>
|
||||
<p class="font-medium text-lg">{$t(list.title)}</p>
|
||||
</div>
|
||||
<p class={`text-sm`}>{list.desc}</p>
|
||||
<p class={`text-sm`}>
|
||||
{$t(list.desc)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-full lg:w-[30%]">
|
||||
<PrimaryButton
|
||||
@@ -118,7 +120,7 @@
|
||||
{#if list.is_completed}
|
||||
<CheckmarkOutline />
|
||||
{/if}
|
||||
{list.button_label}
|
||||
{$t(list.button_label)}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,45 +95,45 @@ export const load = async ({ params = { slug: '' } }) => {
|
||||
const data = [
|
||||
{
|
||||
id: 'profile',
|
||||
title: 'Upload a profile picture and update username',
|
||||
desc: 'Personalize and a human touch making interactions more personal and memorable',
|
||||
title: 'setup.1.title',
|
||||
desc: 'setup.1.desc',
|
||||
is_completed: false,
|
||||
button_label: 'Update Profile'
|
||||
button_label: 'setup.1.button_label'
|
||||
},
|
||||
{
|
||||
id: 'organization',
|
||||
title: 'Update organisation profile picture',
|
||||
desc: 'Establish a professional and recognizable identity for your organization',
|
||||
title: 'setup.2.title',
|
||||
desc: 'setup.2.desc',
|
||||
is_completed: orgHasAvatarUrl,
|
||||
button_label: 'Update Org Profile'
|
||||
button_label: 'setup.2.button_label'
|
||||
},
|
||||
{
|
||||
id: 'course',
|
||||
title: 'Create Course',
|
||||
desc: 'Create a course that you will share with your students',
|
||||
title: 'setup.3.title',
|
||||
desc: 'setup.4.desc',
|
||||
is_completed: isCourseCreated,
|
||||
button_label: 'Create Course'
|
||||
button_label: 'setup.3.button_label'
|
||||
},
|
||||
{
|
||||
id: 'lesson',
|
||||
title: 'Create a lesson',
|
||||
desc: 'Break your course into lesson that your students can easily understand',
|
||||
title: 'setup.4.title',
|
||||
desc: 'setup.4.desc',
|
||||
is_completed: isLessonCreated,
|
||||
button_label: 'Create Lesson'
|
||||
button_label: 'setup.4.button_label'
|
||||
},
|
||||
{
|
||||
id: 'exercise',
|
||||
title: 'Create an exercise',
|
||||
desc: 'Test your students allow them to demonstarte their understanding of the subject matter',
|
||||
title: 'setup.5.title',
|
||||
desc: 'setup.5.desc',
|
||||
is_completed: isExerciseCreated,
|
||||
button_label: 'Create Assignment'
|
||||
button_label: 'setup.5.button_label'
|
||||
},
|
||||
{
|
||||
id: 'publish',
|
||||
title: 'Publish a course',
|
||||
desc: 'Make your course public and purchaseable ',
|
||||
title: 'setup.6.title',
|
||||
desc: 'setup.6.desc',
|
||||
is_completed: isCoursePublished,
|
||||
button_label: 'Publish Course'
|
||||
button_label: 'setup.6.button_label'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user