feat: move lesson comments under lesson note

This commit is contained in:
Rotimi Best
2025-01-04 05:55:14 +01:00
parent 5f367a9540
commit 32ba251b77
44 changed files with 760 additions and 682 deletions
@@ -1,241 +0,0 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import CloseButton from '$lib/components/Buttons/Close/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import { getSupabase } from '$lib/utils/functions/supabase';
import type {
RealtimeChannel,
RealtimePostgresChangesPayload,
PostgrestSingleResponse
} from '@supabase/supabase-js';
import type { LessonCommentInsertPayload, GroupPerson, LessonComment } from '$lib/utils/types';
import { lesson } from '$lib/components/Course/components/Lesson/store/lessons';
import { group } from '$lib/components/Course/store';
import { profile } from '$lib/utils/store/user';
import { calDateDiff } from '$lib/utils/functions/date';
import { t } from '$lib/utils/functions/translations';
export let handleClose = () => {};
const supabase = getSupabase();
let comment = '';
let bodyRef: HTMLDivElement;
let lessonCommentChannel: RealtimeChannel;
let comments: LessonComment[] = [];
let groupmember: GroupPerson | undefined;
let isSaving: boolean = false;
let isFetched = false;
interface FetchComments {
comment: string;
created_at: string;
groupmember: {
id: string;
profile: {
avatar_url: string;
fullname: string;
};
};
}
async function handleSend() {
console.log('groupmember', groupmember);
if (!comment || !groupmember) {
return;
}
comments = [
...comments,
{
comment: comment,
name: $t('course.navItem.lessons.comments.you'),
avatar: $profile.avatar_url,
commentAt: new Date()
}
];
$lesson.totalComments = comments.length;
scrollToBottom();
isSaving = true;
supabase
.from('lesson_comment')
.insert({
lesson_id: $lesson.id,
groupmember_id: groupmember.id,
comment
})
.then(() => {
isSaving = false;
comment = '';
});
}
function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Enter') {
handleSend();
}
}
function scrollToBottom() {
setTimeout(() => {
bodyRef.scrollTo({
top: bodyRef.scrollHeight
});
}, 100);
}
async function handleInsert(payload: RealtimePostgresChangesPayload<LessonCommentInsertPayload>) {
const insertedComment = payload.new as LessonCommentInsertPayload;
if (groupmember && groupmember.id === insertedComment.groupmember_id) {
console.log('is my comment');
return;
}
const {
data,
error
}: PostgrestSingleResponse<{
id: string;
profile: {
fullname: string;
avatar_url: string;
};
}> = await supabase
.from('groupmember')
.select('id, profile:profile_id(fullname, avatar_url)')
.eq('id', insertedComment.groupmember_id)
.single();
console.log('data', data);
console.log('error', error);
if (error || !data) {
return;
}
comments = [
...comments,
{
comment: insertedComment.comment,
name: data.profile.fullname,
avatar: data.profile.avatar_url,
commentAt: insertedComment.created_at
}
];
$lesson.totalComments = comments.length;
scrollToBottom();
}
async function fetchComments(people: GroupPerson[], lessonId?: string | null) {
groupmember = people.find((person) => person.profile_id === $profile.id);
if (!groupmember || !lessonId) return;
const { data, error } = await supabase
.from('lesson_comment')
.select(
`
groupmember_id,
created_at,
comment,
groupmember:groupmember_id(
id,
profile:profile_id(fullname, avatar_url)
)
`
)
.match({
lesson_id: lessonId
})
.returns<FetchComments[]>();
if (error || !data) {
console.error('error');
return;
}
console.log('fetch comments', data);
comments = data.map((lessonComment) => {
return {
comment: lessonComment.comment,
avatar: lessonComment.groupmember.profile.avatar_url,
commentAt: lessonComment.created_at,
name:
lessonComment.groupmember.id === groupmember?.id
? $t('course.navItem.lessons.comments.you')
: lessonComment.groupmember.profile.fullname
};
});
isFetched = true;
scrollToBottom();
}
onMount(async () => {
lessonCommentChannel = supabase
.channel('any')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'lesson_comment' },
handleInsert
)
.subscribe();
});
onDestroy(() => {
supabase.removeChannel(lessonCommentChannel);
});
$: !isFetched && fetchComments($group.people, $lesson.id);
</script>
<PageNav title={$t('course.navItem.lessons.comments.title')} overidableStyle="padding: 0 10px">
<div slot="widget">
<CloseButton onClick={handleClose} />
</div>
</PageNav>
<div bind:this={bodyRef} class="overflow-y-auto h-[87%] pb-10 px-2 md:min-w-[350px] w-full">
{#each comments as comment}
<div class="pb-2 mt-2">
<div class="flex items-start">
<img
alt="Placeholder"
class="block rounded-full"
width="24"
height="20"
src={comment.avatar}
/>
<div class="ml-2">
<p class="dark:text-white text-sm font-bold">{comment.name}</p>
<p class="dark:text-white text-xs font-normal text-gray-800">
{calDateDiff(comment.commentAt)}
</p>
</div>
</div>
<article class="prose prose-sm sm:prose ml-8 max-w-[300px] dark:text-white">
{comment.comment}
</article>
</div>
{/each}
</div>
<div class="footer">
<TextField
placeholder={$t('course.navItem.lessons.comments.placeholder')}
bind:value={comment}
onKeyDown={handleKeyDown}
isDisabled={isSaving}
/>
</div>
<style>
.footer {
position: absolute;
bottom: 0px;
width: 100%;
}
</style>
@@ -1,6 +1,6 @@
<script> <script>
import PageNav from '$lib/components/PageNav/index.svelte';
import CloseButton from '$lib/components/Buttons/Close/index.svelte'; import CloseButton from '$lib/components/Buttons/Close/index.svelte';
import { PageNav } from '$lib/components/Page';
export let handleClose; export let handleClose;
</script> </script>
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import CloseButton from '$lib/components/Buttons/Close/index.svelte'; import CloseButton from '$lib/components/Buttons/Close/index.svelte';
import { course, group } from '$lib/components/Course/store'; import { course, group } from '$lib/components/Course/store';
import PageNav from '$lib/components/PageNav/index.svelte'; import { PageNav } from '$lib/components/Page';
import { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
import { getSupabase } from '$lib/utils/functions/supabase'; import { getSupabase } from '$lib/utils/functions/supabase';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
@@ -1,9 +1,9 @@
<script> <script>
import CheckmarkOutline from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import CloseButton from '$lib/components/Buttons/Close/index.svelte'; import CloseButton from '$lib/components/Buttons/Close/index.svelte';
import Vote from '$lib/components/Vote/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import { PageNav } from '$lib/components/Page';
import Vote from '$lib/components/Vote/index.svelte';
import CheckmarkOutline from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
export let handleClose; export let handleClose;
let bodyRef; let bodyRef;
@@ -95,10 +95,10 @@
<div bind:this={bodyRef} class="body"> <div bind:this={bodyRef} class="body">
{#each questions as question} {#each questions as question}
<div class="flex items-start ml-2 mb-2 mt-2"> <div class="mb-2 ml-2 mt-2 flex items-start">
<Vote bind:value={question.vote} /> <Vote bind:value={question.vote} />
<div class="ml-2 flex-grow"> <div class="ml-2 flex-grow">
<p class="dark:text-white text-sm font-bold">{question.name}</p> <p class="text-sm font-bold dark:text-white">{question.name}</p>
<article class="prose prose-sm sm:prose"> <article class="prose prose-sm sm:prose">
{question.question} {question.question}
</article> </article>
@@ -1,24 +1,21 @@
<script lang="ts"> <script lang="ts">
import IconButton from '$lib/components/IconButton/index.svelte';
import { globalStore } from '$lib/utils/store/app';
import ChartPie from 'carbon-icons-svelte/lib/ChartPie.svelte';
import Settings from 'carbon-icons-svelte/lib/Settings.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import SendAlt from 'carbon-icons-svelte/lib/SendAlt.svelte';
import ChartPie from 'carbon-icons-svelte/lib/ChartPie.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
import Settings from 'carbon-icons-svelte/lib/Settings.svelte';
import { apps } from './store'; import { apps } from './store';
import { globalStore } from '$lib/utils/store/app';
import { browser } from '$app/environment';
import Chip from '$lib/components/Chip/index.svelte'; import Chip from '$lib/components/Chip/index.svelte';
import QandA from './components/QandA/index.svelte'; import { course } from '$lib/components/Course/store';
import Comments from './components/Comments/index.svelte'; import { t } from '$lib/utils/functions/translations';
import { currentOrg } from '$lib/utils/store/org';
import Notes from './components/Notes/index.svelte'; import Notes from './components/Notes/index.svelte';
import Poll from './components/Poll/index.svelte'; import Poll from './components/Poll/index.svelte';
import QandA from './components/QandA/index.svelte';
import APPS_CONSTANTS from './constants'; import APPS_CONSTANTS from './constants';
import { browser } from '$app/environment';
import { course } from '$lib/components/Course/store';
import { lesson } from '$lib/components/Course/components/Lesson/store/lessons';
import { currentOrg } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations';
let appBarRef: HTMLDivElement; let appBarRef: HTMLDivElement;
let appContentRef: HTMLDivElement; let appContentRef: HTMLDivElement;
@@ -70,7 +67,7 @@
bind:this={appBarRef} bind:this={appBarRef}
> >
<div class="apps"> <div class="apps">
<div class="lg:hidden mb-2"> <div class="mb-2 lg:hidden">
<IconButton <IconButton
buttonClassName="lg:hidden" buttonClassName="lg:hidden"
toolTipProps={{ title: 'Settings', hotkeys: [] }} toolTipProps={{ title: 'Settings', hotkeys: [] }}
@@ -80,29 +77,6 @@
</IconButton> </IconButton>
</div> </div>
<div class="mb-2 relative {getAppClass(APPS_CONSTANTS.APPS.COMMENTS, $apps.selectedApp)}">
<IconButton
toolTipProps={{
title: `${
$globalStore.isOrgSite && !$currentOrg.customization.apps.comments
? $t('course.navItem.lessons.disabled')
: $t('course.navItem.lessons.comments.title')
}`,
hotkeys: ['Ctrl/Command', '1']
}}
value={APPS_CONSTANTS.APPS.COMMENTS}
onClick={handleAppClick}
buttonClassName="relative"
disabled={$globalStore.isOrgSite && !$currentOrg.customization.apps.comments}
>
<Chip
value={$lesson.totalComments}
className="absolute -top-1 right-0 bg-gray-100 text-gray-600 dark:bg-neutral-700 dark:text-white"
/>
<SendAlt size={24} class="carbon-icon dark:text-white" />
</IconButton>
</div>
<div class="mb-2 {getAppClass(APPS_CONSTANTS.APPS.POLL, $apps.selectedApp)}"> <div class="mb-2 {getAppClass(APPS_CONSTANTS.APPS.POLL, $apps.selectedApp)}">
<IconButton <IconButton
toolTipProps={{ toolTipProps={{
@@ -149,15 +123,13 @@
{#if !!$apps.selectedApp} {#if !!$apps.selectedApp}
<div <div
class="app max-w-[350px] transition ease-in-out delay-150 duration-100" class="app max-w-[350px] transition delay-150 duration-100 ease-in-out"
bind:this={appContentRef} bind:this={appContentRef}
> >
{#key $apps.selectedApp} {#key $apps.selectedApp}
<div class="h-full w-full" transition:fade={{ duration: 200 }}> <div class="h-full w-full" transition:fade={{ duration: 200 }}>
{#if $apps.selectedApp === APPS_CONSTANTS.APPS.QANDA} {#if $apps.selectedApp === APPS_CONSTANTS.APPS.QANDA}
<QandA {handleClose} /> <QandA {handleClose} />
{:else if $apps.selectedApp === APPS_CONSTANTS.APPS.COMMENTS}
<Comments {handleClose} />
{:else if $apps.selectedApp === APPS_CONSTANTS.APPS.POLL} {:else if $apps.selectedApp === APPS_CONSTANTS.APPS.POLL}
<Poll {handleClose} /> <Poll {handleClose} />
{:else if $apps.selectedApp === APPS_CONSTANTS.APPS.NOTES} {:else if $apps.selectedApp === APPS_CONSTANTS.APPS.NOTES}
@@ -1,5 +1,5 @@
<script> <script>
import PageNav from '$lib/components/PageNav/index.svelte'; import { PageNav } from '$lib/components/Page';
</script> </script>
<PageNav title="Attendance" /> <PageNav title="Attendance" />
@@ -2,7 +2,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import IconButton from '$lib/components/IconButton/index.svelte'; import IconButton from '$lib/components/IconButton/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte'; import { PageBody } from '$lib/components/Page';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte'; import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
@@ -1,24 +1,24 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import Box from '$lib/components/Box/index.svelte';
import { Breadcrumb, BreadcrumbItem } from 'carbon-components-svelte';
import { Moon } from 'svelte-loading-spinners';
import Backdrop from '$lib/components/Backdrop/index.svelte'; import Backdrop from '$lib/components/Backdrop/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import Box from '$lib/components/Box/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte';
import Exercise from '../Exercise/index.svelte';
import { supabase } from '$lib/utils/functions/supabase';
import { createExercise, createExerciseFromTemplate } from '$lib/utils/services/courses';
import { QUESTION_TYPES } from '$lib/components/Question/constants';
import { lesson } from '../store/lessons';
import { questionnaire, isQuestionnaireFetching } from '../store/exercise';
import NewExerciseModal from '$lib/components/Course/components/Lesson/Exercises/NewExerciseModal.svelte'; import NewExerciseModal from '$lib/components/Course/components/Lesson/Exercises/NewExerciseModal.svelte';
import type { ExerciseTemplate } from '$lib/utils/types'; import { PageBody } from '$lib/components/Page';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { QUESTION_TYPES } from '$lib/components/Question/constants';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import { formatDate } from '$lib/utils/functions/routes/dashboard'; import { formatDate } from '$lib/utils/functions/routes/dashboard';
import { supabase } from '$lib/utils/functions/supabase';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
import { createExercise, createExerciseFromTemplate } from '$lib/utils/services/courses';
import { globalStore } from '$lib/utils/store/app'; import { globalStore } from '$lib/utils/store/app';
import type { ExerciseTemplate } from '$lib/utils/types';
import { Breadcrumb, BreadcrumbItem } from 'carbon-components-svelte';
import { onMount } from 'svelte';
import { Moon } from 'svelte-loading-spinners';
import Exercise from '../Exercise/index.svelte';
import { isQuestionnaireFetching, questionnaire } from '../store/exercise';
import { lesson } from '../store/lessons';
export let path = ''; export let path = '';
export let exerciseId = ''; export let exerciseId = '';
@@ -0,0 +1,367 @@
<script lang="ts">
import Avatar from '$lib/components/Avatar/index.svelte';
import { lesson } from '$lib/components/Course/components/Lesson/store/lessons';
import { group } from '$lib/components/Course/store';
import TextArea from '$lib/components/Form/TextArea.svelte';
import DeleteModal from '$lib/components/Modal/DeleteModal.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { snackbar } from '$lib/components/Snackbar/store';
import { calDateDiff } from '$lib/utils/functions/date';
import { getSupabase } from '$lib/utils/functions/supabase';
import { t } from '$lib/utils/functions/translations';
import { profile } from '$lib/utils/store/user';
import type { GroupPerson, LessonComment, LessonCommentInsertPayload } from '$lib/utils/types';
import type {
PostgrestSingleResponse,
RealtimeChannel,
RealtimePostgresChangesPayload
} from '@supabase/supabase-js';
import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
import { onDestroy, onMount } from 'svelte';
const supabase = getSupabase();
const PAGE_SIZE = 20;
export let lessonId = '';
let comment = '';
let lessonCommentChannel: RealtimeChannel;
let comments: LessonComment[] = [];
let groupmember: GroupPerson | undefined;
let isSaving: boolean = false;
let isFetching = false;
let openDeleteModal = false;
let deleteCommentId: number | null = null;
let editCommentId: number | null = null;
interface FetchComments {
id: number;
comment: string;
created_at: string;
groupmember: {
id: string;
profile: {
avatar_url: string;
fullname: string;
};
};
}
async function handleSend() {
if (!comment || !groupmember) {
return;
}
isSaving = true;
comments = [
{
id: 0,
comment: comment,
name: $t('course.navItem.lessons.comments.you'),
avatar: $profile.avatar_url,
commentAt: new Date(),
groupmember_id: groupmember.id
},
...comments
];
pagination.count = comments.length;
supabase
.from('lesson_comment')
.insert({
lesson_id: $lesson.id,
groupmember_id: groupmember.id,
comment
})
.select('id')
.single()
.then(({ data, error }) => {
isSaving = false;
if (error) {
console.error('Error adding comment:', error);
return;
}
comments = comments.map((comment) =>
comment.id === 0 ? { ...comment, id: data.id } : comment
);
isSaving = false;
comment = '';
});
}
async function handleUpdate(commentItem: LessonComment) {
if (!editCommentId || !commentItem.comment) {
return;
}
const { error } = await supabase
.from('lesson_comment')
.update({ comment: commentItem.comment })
.eq('id', editCommentId);
if (error) {
console.error('handleUpdate', error);
snackbar.error($t('snackbar.something'));
} else {
snackbar.success($t('snackbar.success_update'));
}
editCommentId = null;
pagination.count = comments.length;
}
async function handleDeleteComment() {
if (!deleteCommentId) {
return;
}
const { error } = await supabase.from('lesson_comment').delete().eq('id', deleteCommentId);
if (error) {
console.error('error', error);
snackbar.error($t('snackbar.something'));
} else {
snackbar.success($t('snackbar.success_delete'));
}
comments = comments.filter((comment) => comment.id !== deleteCommentId);
pagination.count = comments.length;
deleteCommentId = null;
}
async function handleInsert(payload: RealtimePostgresChangesPayload<LessonCommentInsertPayload>) {
const insertedComment = payload.new as LessonCommentInsertPayload;
if (groupmember && groupmember.id === insertedComment.groupmember_id) {
return;
}
const {
data,
error
}: PostgrestSingleResponse<{
id: string;
profile: {
fullname: string;
avatar_url: string;
};
}> = await supabase
.from('groupmember')
.select('id, profile:profile_id(fullname, avatar_url)')
.eq('id', insertedComment.groupmember_id)
.single();
if (error || !data) {
console.error('handleInsert', error);
return;
}
comments = [
{
id: insertedComment.id,
comment: insertedComment.comment,
name: data.profile.fullname,
avatar: data.profile.avatar_url,
commentAt: insertedComment.created_at,
groupmember_id: insertedComment.groupmember_id
},
...comments
];
pagination.count = comments.length;
}
let pagination = {
hasMore: true,
count: 0,
page: 0
};
async function fetchComments(people: GroupPerson[]) {
if (!pagination.hasMore) return;
groupmember = people.find((person) => person.profile_id === $profile.id);
if (!groupmember || !lessonId) return;
isFetching = true;
const from = pagination.page * PAGE_SIZE;
const to = from + PAGE_SIZE - 1;
const { data, error, count } = await supabase
.from('lesson_comment')
.select(
`
groupmember_id,
created_at,
id,
comment,
groupmember:groupmember_id(
id,profile:profile_id(fullname, avatar_url)
)
`,
{ count: 'exact' }
)
.match({
lesson_id: lessonId
})
.order('created_at', { ascending: false })
.range(from, to)
.returns<FetchComments[]>();
if (error || !data) {
console.error('error');
return;
}
const newComments = data.map((lessonComment) => {
return {
id: lessonComment.id,
comment: lessonComment.comment,
avatar: lessonComment.groupmember.profile.avatar_url,
commentAt: lessonComment.created_at,
groupmember_id: lessonComment.groupmember.id,
name:
lessonComment.groupmember.id === groupmember?.id
? $t('course.navItem.lessons.comments.you')
: lessonComment.groupmember.profile.fullname
};
});
comments = pagination.page === 0 ? newComments : [...comments, ...newComments];
pagination.hasMore = count ? from + data.length < count : false;
pagination.count = count ?? 0;
pagination.page++;
isFetching = false;
}
onMount(async () => {
fetchComments($group.people);
lessonCommentChannel = supabase
.channel('any')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'lesson_comment' },
handleInsert
)
.subscribe();
});
onDestroy(() => {
supabase.removeChannel(lessonCommentChannel);
});
</script>
<DeleteModal bind:open={openDeleteModal} onDelete={handleDeleteComment} />
<div class="mx-auto mt-10 max-w-[65ch]">
<!-- <hr class="my-5" /> -->
<div class="mb-5">
<p class="text-xl font-bold capitalize">
{$t('course.navItem.lessons.comments.title')} ({pagination.count})
</p>
</div>
<div>
<div class="flex h-full items-start gap-3">
<Avatar
src={$profile.avatar_url}
name={$profile.fullname}
width="w-8"
height="h-8"
className="mt-2"
/>
<div class="h-full w-full">
<TextArea
label={$t('course.navItem.lessons.comments.text_area_title')}
placeholder={$t('course.navItem.lessons.comments.placeholder')}
bind:value={comment}
/>
</div>
</div>
<div class="mt-2 flex flex-row-reverse">
<PrimaryButton
label={$t('course.navItem.lessons.comments.comment_btn')}
onClick={handleSend}
isDisabled={!comment}
isLoading={isSaving}
/>
</div>
</div>
<div class="my-10">
{#each comments as commentItem}
<div class="mt-2 flex items-start gap-3 pb-2">
<Avatar src={commentItem.avatar} name={commentItem.name} width="w-8" height="h-8" />
<div class="w-full rounded-md border px-4 pb-4 pt-2 dark:border-neutral-700">
<div class="flex items-center justify-between gap-2">
<p class="text-md font-bold dark:text-white">
{commentItem.name}
<span
class="ml-1 text-xs font-normal text-gray-800 dark:text-white"
title={commentItem.commentAt.toLocaleString()}
>
{calDateDiff(commentItem.commentAt)}
</span>
</p>
{#if groupmember?.id === commentItem.groupmember_id}
<OverflowMenu flipped>
<OverflowMenuItem text="Edit" on:click={() => (editCommentId = commentItem.id)} />
<OverflowMenuItem
danger
text="Delete"
on:click={() => {
deleteCommentId = commentItem.id;
openDeleteModal = true;
}}
/>
</OverflowMenu>
{/if}
</div>
{#if editCommentId === commentItem.id}
<TextArea
placeholder={$t('course.navItem.lessons.comments.placeholder')}
bind:value={commentItem.comment}
/>
<div class="mt-2 flex flex-row-reverse items-center gap-2">
<PrimaryButton
variant={VARIANTS.OUTLINED}
label={$t('course.navItem.lessons.comments.cancel_btn')}
onClick={() => (editCommentId = null)}
/>
<PrimaryButton
label={$t('course.navItem.lessons.comments.comment_btn')}
onClick={() => handleUpdate(commentItem)}
isDisabled={!commentItem.comment}
isLoading={isSaving}
/>
</div>
{:else}
<article class="prose sm:prose-sm max-w-[300px] dark:text-white">
{commentItem.comment}
</article>
{/if}
</div>
</div>
{/each}
</div>
{#if pagination.hasMore}
<div class="mt-2 flex items-center justify-center">
<PrimaryButton
label={$t('course.navItem.lessons.comments.load_more_btn')}
variant={VARIANTS.OUTLINED}
onClick={() => fetchComments($group.people)}
isLoading={isFetching}
/>
</div>
{/if}
</div>
@@ -1,6 +1,14 @@
import { Video, Notebook, PresentationFile } from 'carbon-icons-svelte'; import type { Tabs } from '$lib/utils/types';
import { Notebook, PresentationFile, Video } from 'carbon-icons-svelte';
export let tabs = [ interface MaterialTab {
label: string;
icon: any;
value: number;
badgeValue?: number;
}
export let tabs: MaterialTab[] = [
{ {
label: 'course.navItem.lessons.materials.tabs.note.title', label: 'course.navItem.lessons.materials.tabs.note.title',
icon: Notebook, icon: Notebook,
@@ -21,9 +29,9 @@ export let tabs = [
} }
]; ];
export function orderedTabs(tabs, settingTabs) { export function orderedTabs(tabs: MaterialTab[], settingTabs?: Tabs[]) {
if (!Array.isArray(settingTabs)) return tabs; if (!Array.isArray(settingTabs)) return tabs;
const reorderedTabs = []; const reorderedTabs: MaterialTab[] = [];
const tabMap = new Map(tabs.map((tab) => [tab.value, tab])); const tabMap = new Map(tabs.map((tab) => [tab.value, tab]));
@@ -1,47 +1,48 @@
<script lang="ts"> <script lang="ts">
import isEmpty from 'lodash/isEmpty';
import { fade } from 'svelte/transition';
import { useCompletion } from 'ai/svelte';
import MODES from '$lib/utils/constants/mode';
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
import { formatYoutubeVideo } from '$lib/utils/functions/formatYoutubeVideo';
import Modal from '$lib/components/Modal/index.svelte';
import { Popover } from 'carbon-components-svelte';
import AlignBoxTopLeftIcon from 'carbon-icons-svelte/lib/AlignBoxTopLeft.svelte';
import ListIcon from 'carbon-icons-svelte/lib/List.svelte';
import IbmWatsonKnowledgeStudioIcon from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeStudio.svelte';
import MagicWandFilled from 'carbon-icons-svelte/lib/MagicWandFilled.svelte';
import Tabs from '$lib/components/Tabs/index.svelte';
import TabContent from '$lib/components/TabContent/index.svelte';
import Box from '$lib/components/Box/index.svelte'; import Box from '$lib/components/Box/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import VideoUploader from '$lib/components/Course/components/Lesson/Materials/Video/Index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import TextField from '$lib/components/Form/TextField.svelte';
import { import {
lesson, deleteLessonVideo,
lessons,
lessonByTranslation,
handleUpdateLessonMaterials, handleUpdateLessonMaterials,
isLessonDirty, isLessonDirty,
uploadCourseVideoStore, lesson,
deleteLessonVideo lessonByTranslation,
lessons,
uploadCourseVideoStore
} from '$lib/components/Course/components/Lesson/store/lessons'; } from '$lib/components/Course/components/Lesson/store/lessons';
import VideoUploader from '$lib/components/Course/components/Lesson/Materials/Video/Index.svelte';
import { course } from '$lib/components/Course/store'; import { course } from '$lib/components/Course/store';
import TextField from '$lib/components/Form/TextField.svelte';
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
import Modal from '$lib/components/Modal/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { snackbar } from '$lib/components/Snackbar/store';
import TabContent from '$lib/components/TabContent/index.svelte';
import Tabs from '$lib/components/Tabs/index.svelte';
import TextEditor from '$lib/components/TextEditor/index.svelte'; import TextEditor from '$lib/components/TextEditor/index.svelte';
import * as CONSTANTS from './constants'; import MODES from '$lib/utils/constants/mode';
import { orderedTabs } from './constants'; import { formatYoutubeVideo } from '$lib/utils/functions/formatYoutubeVideo';
import { supabase } from '$lib/utils/functions/supabase';
import { isHtmlValueEmpty } from '$lib/utils/functions/toHtml';
import { lessonFallbackNote, t } from '$lib/utils/functions/translations';
import { currentOrg } from '$lib/utils/store/org';
import type { LessonPage, LOCALE } from '$lib/utils/types';
import { useCompletion } from 'ai/svelte';
import { Popover } from 'carbon-components-svelte';
import AlignBoxTopLeftIcon from 'carbon-icons-svelte/lib/AlignBoxTopLeft.svelte';
import IbmWatsonKnowledgeStudioIcon from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeStudio.svelte';
import ListIcon from 'carbon-icons-svelte/lib/List.svelte';
import MagicWandFilled from 'carbon-icons-svelte/lib/MagicWandFilled.svelte';
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
import isEmpty from 'lodash/isEmpty';
import { fade } from 'svelte/transition';
import Comments from './components/Comments.svelte';
import ComponentNote from './components/ComponentNote.svelte'; import ComponentNote from './components/ComponentNote.svelte';
import ComponentSlide from './components/ComponentSlide.svelte'; import ComponentSlide from './components/ComponentSlide.svelte';
import ComponentVideo from './components/ComponentVideo.svelte'; import ComponentVideo from './components/ComponentVideo.svelte';
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte'; import * as CONSTANTS from './constants';
import type { LessonPage } from '$lib/utils/types'; import { orderedTabs } from './constants';
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'; import Loader from './Loader.svelte';
export let mode = MODES.view; export let mode = MODES.view;
@@ -279,7 +280,6 @@
const componentNames = tabs const componentNames = tabs
.map((tab) => { .map((tab) => {
// @ts-ignore
const component = componentMap[tab.value]; const component = componentMap[tab.value];
return component || null; return component || null;
}) })
@@ -320,7 +320,7 @@
<HtmlRender className="m-auto text-center"> <HtmlRender className="m-auto text-center">
<svelte:fragment slot="content"> <svelte:fragment slot="content">
<h1 class="text-2xl md:text-4xl mt-0 capitalize"> <h1 class="mt-0 text-2xl capitalize md:text-4xl">
{lessonTitle} {lessonTitle}
</h1> </h1>
</svelte:fragment> </svelte:fragment>
@@ -335,7 +335,7 @@
value={getValue('course.navItem.lessons.materials.tabs.note.title')} value={getValue('course.navItem.lessons.materials.tabs.note.title')}
index={currentTab} index={currentTab}
> >
<div class="flex gap-1 justify-end"> <div class="flex justify-end gap-1">
<div bind:this={aiButtonRef} class="flex flex-row-reverse"> <div bind:this={aiButtonRef} class="flex flex-row-reverse">
<PrimaryButton <PrimaryButton
className="flex items-center relative" className="flex items-center relative"
@@ -376,7 +376,7 @@
</div> </div>
</div> </div>
<div class="h-[60vh] mt-5"> <div class="mt-5 h-[60vh]">
<TextEditor <TextEditor
id={lessonId} id={lessonId}
bind:editorWindowRef bind:editorWindowRef
@@ -418,7 +418,7 @@
className="mb-2" className="mb-2"
/> />
{#if $lesson.materials.videos.length} {#if $lesson.materials.videos.length}
<div class="flex flex-col items-start w-full h-full"> <div class="flex h-full w-full flex-col items-start">
{#each $lesson.materials.videos as video, index} {#each $lesson.materials.videos as video, index}
{#if mode === MODES.edit} {#if mode === MODES.edit}
<div class="ml-auto"> <div class="ml-auto">
@@ -431,7 +431,7 @@
</IconButton> </IconButton>
</div> </div>
{/if} {/if}
<div class="w-full h-full flex flex-col gap-2 overflow-hidden"> <div class="flex h-full w-full flex-col gap-2 overflow-hidden">
{#key video.link} {#key video.link}
<div class="mb-5"> <div class="mb-5">
{#if video.type === 'youtube'} {#if video.type === 'youtube'}
@@ -484,21 +484,26 @@
</Tabs> </Tabs>
{:else if !isMaterialsEmpty($lesson.materials, $lessonByTranslation[lessonId])} {:else if !isMaterialsEmpty($lesson.materials, $lessonByTranslation[lessonId])}
{#key lessonId} {#key lessonId}
<div class="w-full mb-20" in:fade={{ delay: 500 }} out:fade> <div class="mb-20 w-full" in:fade={{ delay: 500 }} out:fade>
{#each componentsToRender as Component} {#each componentsToRender as Component}
<svelte:component this={Component} {lessonId} /> <svelte:component this={Component} {lessonId} />
{/each} {/each}
{#if $currentOrg.customization.apps.comments}
<hr class="my-5" />
<Comments {lessonId} />
{/if}
</div> </div>
{/key} {/key}
{:else} {:else}
<Box className="text-center"> <Box className="text-center">
<img src="/no-video.svg" alt="Video not found" /> <img src="/no-video.svg" alt="Video not found" />
<h3 class="text-xl font-normal dark:text-white py-2"> <h3 class="py-2 text-xl font-normal dark:text-white">
{$t('course.navItem.lessons.materials.body_heading')} {$t('course.navItem.lessons.materials.body_heading')}
</h3> </h3>
{#if !isStudent} {#if !isStudent}
<p class="text-sm text-center font-normal py-2"> <p class="py-2 text-center text-sm font-normal">
{$t('course.navItem.lessons.materials.body_content')} {$t('course.navItem.lessons.materials.body_content')}
<strong>{$t('course.navItem.lessons.materials.get_started')}</strong> <strong>{$t('course.navItem.lessons.materials.get_started')}</strong>
{$t('course.navItem.lessons.materials.button')}. {$t('course.navItem.lessons.materials.button')}.
@@ -8,7 +8,7 @@
<article <article
style:max-width={disableMaxWidth ? 'unset' : undefined} style:max-width={disableMaxWidth ? 'unset' : undefined}
{id} {id}
class="preview prose prose-sm sm:prose {className} dark:text-white" class="preview prose sm:prose-sm {className} dark:text-white"
> >
{#if $$slots.content} {#if $$slots.content}
<slot name="content" /> <slot name="content" />
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import Modal from './index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
import Modal from './index.svelte';
export let open = false; export let open = false;
export let onDelete = () => {}; export let onDelete = () => {};
@@ -22,7 +22,7 @@
modalHeading={$t('delete_modal.label')} modalHeading={$t('delete_modal.label')}
> >
<div> <div>
<p class="dark:text-white text-xl text-center"> <p class="text-center text-xl dark:text-white">
{$t('delete_modal.content')} {$t('delete_modal.content')}
</p> </p>
@@ -1,19 +1,18 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Box from '$lib/components/Box/index.svelte';
import { courses } from '$lib/components/Courses/store';
import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte';
import Vote from '$lib/components/Vote/index.svelte';
import { calDateDiff } from '$lib/utils/functions/date';
import { supabase } from '$lib/utils/functions/supabase';
import { t } from '$lib/utils/functions/translations';
import { fetchCourses } from '$lib/utils/services/courses';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
import { profile } from '$lib/utils/store/user';
import { Dropdown, Search } from 'carbon-components-svelte';
import AddCommentIcon from 'carbon-icons-svelte/lib/AddComment.svelte'; import AddCommentIcon from 'carbon-icons-svelte/lib/AddComment.svelte';
import CommunityLoader from './Loader.svelte'; import CommunityLoader from './Loader.svelte';
import Vote from '$lib/components/Vote/index.svelte';
import Box from '$lib/components/Box/index.svelte';
import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte';
import Space from '$lib/components/Space/index.svelte';
import { currentOrgPath, currentOrg } from '$lib/utils/store/org';
import { supabase } from '$lib/utils/functions/supabase';
import { calDateDiff } from '$lib/utils/functions/date';
import { fetchCourses } from '$lib/utils/services/courses';
import { profile } from '$lib/utils/store/user';
import { Search, Dropdown } from 'carbon-components-svelte';
import { t } from '$lib/utils/functions/translations';
import { courses } from '$lib/components/Courses/store';
export let isLMS = false; export let isLMS = false;
@@ -97,7 +96,7 @@
class=" bg-gray-100 dark:bg-neutral-800" class=" bg-gray-100 dark:bg-neutral-800"
/> />
<Dropdown <Dropdown
class="w-[25%] h-full" class="h-full w-[25%]"
size="xl" size="xl"
label={$t('community.ask.select_course')} label={$t('community.ask.select_course')}
items={[ items={[
@@ -108,7 +107,7 @@
/> />
</div> </div>
<div <div
class="flex items-center justify-center lg:justify-start flex-wrap my-4 m-auto border-c rounded bg-gray-100 dark:bg-neutral-800" class="border-c m-auto my-4 flex flex-wrap items-center justify-center rounded bg-gray-100 dark:bg-neutral-800 lg:justify-start"
> >
{#if isLoading} {#if isLoading}
<CommunityLoader /> <CommunityLoader />
@@ -117,9 +116,9 @@
<CommunityLoader /> <CommunityLoader />
{:else} {:else}
{#each filteredDiscussions as discussion} {#each filteredDiscussions as discussion}
<div class="w-full flex border-bottom-c p-3"> <div class="border-bottom-c flex w-full p-3">
<Vote value={discussion.votes} /> <Vote value={discussion.votes} />
<div class="text-sm flex flex-col gap-y-0.5"> <div class="flex flex-col gap-y-0.5 text-sm">
<h4 class="mt-0"> <h4 class="mt-0">
<a <a
class="text-black dark:text-white" class="text-black dark:text-white"
@@ -132,12 +131,12 @@
{discussion.author} asked {discussion.createdAt} {discussion.author} asked {discussion.createdAt}
</span> </span>
<a class="m-0" href="/courses/{discussion.courseId}"> <a class="m-0" href="/courses/{discussion.courseId}">
<span class="text-xs text-primary-200 dark:text-black text-primary-700 p-0"> <span class="text-primary-200 text-primary-700 p-0 text-xs dark:text-black">
#{discussion.courseTitle} #{discussion.courseTitle}
</span> </span>
</a> </a>
</div> </div>
<Space /> <div class="flex-grow" />
<div class="flex items-center"> <div class="flex items-center">
<AddCommentIcon size={20} /> <AddCommentIcon size={20} />
<span class="ml-1">{discussion.comments}</span> <span class="ml-1">{discussion.comments}</span>
@@ -1,6 +1,4 @@
<script> <script>
// import
// border border-gray-300 rounded-md p-3 bg-primary-200 bg-opacity-25
export let width = 'max-w-4xl w-full lg:w-11/12'; export let width = 'max-w-4xl w-full lg:w-11/12';
export let padding = 'pb-5 px-4'; export let padding = 'pb-5 px-4';
export let className = ''; export let className = '';
@@ -14,12 +12,12 @@
<div <div
class="overflow-y-auto {isPageNavHidden class="overflow-y-auto {isPageNavHidden
? 'h-[calc(100vh-65px)] lg:h-[calc(100vh-127px)]' ? 'h-[calc(100vh-65px)] lg:h-[calc(100vh-127px)]'
: 'h-[calc(100vh-127px)]'} mt-4 mx-auto {width} {className} relative {padding}" : 'h-[calc(100vh-127px)]'} mx-auto mt-4 {width} {className} relative {padding}"
on:click={onClick} on:click={onClick}
> >
{#if $$slots.header} {#if $$slots.header}
<div <div
class="head dark:bg-neutral-800 flex items-center justify-between sticky right-0 w-full px-3 {headerClassName}" class="head sticky right-0 flex w-full items-center justify-between px-3 dark:bg-neutral-800 {headerClassName}"
> >
<slot name="header" /> <slot name="header" />
</div> </div>
@@ -1,14 +1,11 @@
<script> <script>
import NewButton from '../PrimaryContainedButton/index.svelte';
import TextField from '../Form/TextField.svelte';
import Chip from '../Chip/index.svelte';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
import Chip from '../Chip/index.svelte';
import TextField from '../Form/TextField.svelte';
export let title = ''; export let title = '';
export let overidableStyle = ''; export let overidableStyle = '';
export let navItems = []; export let navItems = [];
export let addButtonHref = '';
export let addButtonLabel = '';
export let disableSticky = false; export let disableSticky = false;
export let isTitleEditable = false; export let isTitleEditable = false;
export let onEditComplete = () => {}; export let onEditComplete = () => {};
@@ -23,21 +20,21 @@
<div <div
class="{hideOnMobile && class="{hideOnMobile &&
'hideOnMobile'} header dark:bg-black dark:border-neutral-600 border-b border-gray-200 bg-white {!disableSticky && 'hideOnMobile'} header border-b border-gray-200 bg-white dark:border-neutral-600 dark:bg-black {!disableSticky &&
'sticky'} {dynamicRootClass}" 'sticky'} {dynamicRootClass}"
style={overidableStyle} style={overidableStyle}
> >
<div <div
class="{hideOnMobile class="{hideOnMobile
? 'hidden lg:flex' ? 'hidden lg:flex'
: ''} {paddingClass} max-w-4xl m-auto flex items-center justify-between min-h-[61px]" : ''} {paddingClass} m-auto flex min-h-[61px] max-w-4xl items-center justify-between"
> >
{#if !!title} {#if !!title}
{#if isTitleEditable} {#if isTitleEditable}
{#if !enterEditTitleMode} {#if !enterEditTitleMode}
<button class="w-full" on:click={() => (enterEditTitleMode = true)}> <button class="w-full" on:click={() => (enterEditTitleMode = true)}>
<h4 <h4
class="editable-title hover:bg-gray-100 dark:bg-black px-3 rounded-md overflow-ellipsis" class="editable-title overflow-ellipsis rounded-md px-3 hover:bg-gray-100 dark:bg-black"
> >
{title} {title}
</h4> </h4>
@@ -56,12 +53,12 @@
/> />
{/if} {/if}
{:else} {:else}
<div class="flex items-center w-full" {title}> <div class="flex w-full items-center" {title}>
<slot name="image" /> <slot name="image" />
<h4 <h4
class="{$$slots.image class="{$$slots.image
? 'ml-2' ? 'ml-2'
: ''} dark:text-white whitespace-nowrap truncate text-lg font-semibold" : ''} truncate whitespace-nowrap text-lg font-semibold dark:text-white"
> >
{title} {title}
</h4> </h4>
@@ -69,12 +66,12 @@
{/if} {/if}
{/if} {/if}
{#if Array.isArray(navItems) && navItems.length} {#if Array.isArray(navItems) && navItems.length}
<div class="flex justify-evenly items-center"> <div class="flex items-center justify-evenly">
{#each navItems as item} {#each navItems as item}
<a <a
class="mr-5 pb-2 text-sm font-bold flex items-center relative {item.isActive class="relative mr-5 flex items-center pb-2 text-sm font-bold {item.isActive
? 'text-primary-700' ? 'text-primary-700'
: 'dark:text-white text-black'} hover:no-underline" : 'text-black dark:text-white'} hover:no-underline"
href={item.href} href={item.href}
> >
{item.label} {item.label}
@@ -83,15 +80,12 @@
<Chip value={item.badgeValue} className="ml-1 bg-gray-300 dark:text-black" /> <Chip value={item.badgeValue} className="ml-1 bg-gray-300 dark:text-black" />
{/if} {/if}
<span <span
class="absolute bottom-0 left-0 h-1 bg-primary-700 transition-all ease-in-out duration-500 {item.isActive class="bg-primary-700 absolute bottom-0 left-0 h-1 transition-all duration-500 ease-in-out {item.isActive
? 'w-full' ? 'w-full'
: 'w-0'}" : 'w-0'}"
/> />
</a> </a>
{/each} {/each}
{#if addButtonHref}
<NewButton href={addButtonHref} label={addButtonLabel} />
{/if}
</div> </div>
{/if} {/if}
<slot name="widget" /> <slot name="widget" />
@@ -1,7 +1,7 @@
<script> <script>
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
export let isOrg = false; export let isOrg = false;
@@ -14,17 +14,17 @@
} }
</script> </script>
<div class="root flex items-center justify-center flex-col w-screen"> <div class="root flex w-screen flex-col items-center justify-center">
<img src="https://brand.cdn.clsrio.com/404.png" alt="classroomio_error_image" /> <img src="https://brand.cdn.clsrio.com/404.png" alt="classroomio_error_image" />
<div class="flex flex-col items-center justify-center dark:text-white gap-3 w-4/5 lg:w-2/5"> <div class="flex w-4/5 flex-col items-center justify-center gap-3 dark:text-white lg:w-2/5">
<p class="text-4xl dark:text-white font-semibold mb-5"> <p class="mb-5 text-4xl font-semibold dark:text-white">
{#if isOrg} {#if isOrg}
Organization doesn't exist! Organization doesn't exist!
{:else} {:else}
Page not found Page not found
{/if} {/if}
</p> </p>
<p class=" text-gray-700 text-lg text-center dark:text-white mb-5"> <p class=" mb-5 text-center text-lg text-gray-700 dark:text-white">
The page you are looking for doesn't exist or has been moved. Please go back to the homepage. The page you are looking for doesn't exist or has been moved. Please go back to the homepage.
</p> </p>
<PrimaryButton <PrimaryButton
@@ -1,15 +1,15 @@
<script> <script>
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
</script> </script>
<div <div
class="root flex flex-col gap-2 items-center justify-center w-screen h-[calc(100vh-48px)] text-center bg-[#F3F5F9] dark:bg-black text-black dark:text-white" class="root flex h-[calc(100vh-48px)] w-screen flex-col items-center justify-center gap-2 bg-[#F3F5F9] text-center text-black dark:bg-black dark:text-white"
> >
<img src="./unauthorized.svg" alt="unauthorized" class="w-[220px]" /> <img src="./unauthorized.svg" alt="unauthorized" class="w-[220px]" />
<p class=" text-black dark:text-white text-4xl md:text-5xl font-bold">Page not found</p> <p class=" text-4xl font-bold text-black dark:text-white md:text-5xl">Page not found</p>
<p class=" text-[#656565] dark:text-white text-base font-[400] mb-2 mt-1"> <p class=" mb-2 mt-1 text-base font-[400] text-[#656565] dark:text-white">
The page you are trying to access does not exist The page you are trying to access does not exist
</p> </p>
@@ -0,0 +1,5 @@
export { default as PageBody } from './Body.svelte';
export { default as PageNav } from './Nav.svelte';
export { default as PageNotFound } from './NotFound.svelte';
export { default as PageRestricted } from './Restricted.svelte';
export { default as PageUnauthorized } from './Unauthorized.svelte';
@@ -1,36 +0,0 @@
<script>
export let href = '';
export let label = '';
export let handleClick = () => {};
</script>
<a
class="root bg-primary-700 hover:bg-primary-900 text-white font-bold rounded inline-flex items-center float-right"
{href}
on:click={handleClick}
>
<!-- <AddAlt32 class="carbon-icon fill-current w-4 h-4 mr-1" /> -->
{label}
</a>
<style lang="scss">
.root {
height: fit-content;
border-radius: 5px;
padding: 4px 20px;
&:hover {
cursor: pointer;
}
}
@media only screen and (max-width: 1002px) {
a {
padding: 10px 15px;
}
a :global(svg.carbon-icon) {
margin-right: 0;
}
}
</style>
@@ -1 +0,0 @@
<div />
@@ -1,9 +0,0 @@
<style>
.actions {
flex-grow: 1;
}
</style>
<div class="actions"></div>
@@ -5,28 +5,28 @@
export let tabs: { export let tabs: {
icon?: any; icon?: any;
label: string; label: string;
value: string; value: string | number;
badgeValue?: number; badgeValue?: number;
}[] = []; }[] = [];
export let currentTab: string | number; export let currentTab: string | number;
export let onChange = (v: string | number) => () => {}; export let onChange = (v: string | number) => () => {};
</script> </script>
<div class="w-full flex flex-col"> <div class="flex w-full flex-col">
<div class="flex items-center border-b w-full overflow-x-auto mb-2"> <div class="mb-2 flex w-full items-center overflow-x-auto border-b">
{#each tabs as tab} {#each tabs as tab}
{#if !tab.icon && !tab.badgeValue} {#if !tab.icon && !tab.badgeValue}
<button <button
class="relative {currentTab === tab.value class="relative {currentTab === tab.value
? 'text-primary-700' ? 'text-primary-700'
: 'dark:bg-gray-500 dark:text-white'} dark:bg-transparent font-semibold focus:outline-none w-fit mr-4 text-center py-3 px-2" : 'dark:bg-gray-500 dark:text-white'} mr-4 w-fit px-2 py-3 text-center font-semibold focus:outline-none dark:bg-transparent"
on:click={onChange(tab.value)} on:click={onChange(tab.value)}
> >
<div class="flex items-center justify-center w-full text-center"> <div class="flex w-full items-center justify-center text-center">
{$t(tab.label)} {$t(tab.label)}
</div> </div>
<span <span
class="absolute bottom-0 left-0 h-[2px] bg-primary-700 transition-all ease-in-out duration-500 {currentTab === class="bg-primary-700 absolute bottom-0 left-0 h-[2px] transition-all duration-500 ease-in-out {currentTab ===
tab.value tab.value
? 'w-full' ? 'w-full'
: 'w-0'}" : 'w-0'}"
@@ -36,10 +36,10 @@
<button <button
class="relative {currentTab === tab.value class="relative {currentTab === tab.value
? 'text-primary-700' ? 'text-primary-700'
: 'dark:bg-gray-500 dark:text-white'} dark:bg-transparent font-semibold focus:outline-none w-24 text-left py-3 mr-8 px-2" : 'dark:bg-gray-500 dark:text-white'} mr-8 w-24 px-2 py-3 text-left font-semibold focus:outline-none dark:bg-transparent"
on:click={onChange(tab.value)} on:click={onChange(tab.value)}
> >
<div class="grid grid-cols-5 md:grid-cols-4 gap-5 md:gap-3 items-center"> <div class="grid grid-cols-5 items-center gap-5 md:grid-cols-4 md:gap-3">
{#if tab.icon} {#if tab.icon}
<svelte:component this={tab.icon} /> <svelte:component this={tab.icon} />
{/if} {/if}
@@ -54,7 +54,7 @@
{/if} {/if}
</div> </div>
<span <span
class="absolute bottom-0 left-0 h-[2px] bg-primary-700 transition-all ease-in-out duration-500 {currentTab === class="bg-primary-700 absolute bottom-0 left-0 h-[2px] transition-all duration-500 ease-in-out {currentTab ===
tab.value tab.value
? 'w-full' ? 'w-full'
: 'w-0'}" : 'w-0'}"
@@ -160,7 +160,8 @@
"generic": { "generic": {
"success_delete": "Erfolgreich gelöscht", "success_delete": "Erfolgreich gelöscht",
"try_again": "Fehler – Bitte versuchen Sie es später noch einmal" "try_again": "Fehler – Bitte versuchen Sie es später noch einmal"
} },
"success_delete": "Erfolgreich gelöscht"
}, },
"exercises": { "exercises": {
"heading": "Übungen", "heading": "Übungen",
@@ -574,7 +575,11 @@
"comments": { "comments": {
"title": "Kommentare", "title": "Kommentare",
"you": "Du", "you": "Du",
"placeholder": "Sag etwas" "placeholder": "Sag etwas",
"text_area_title": "Hinterlassen Sie einen Kommentar",
"comment_btn": "Kommentar",
"load_more_btn": "Mehr laden",
"cancel_btn": "Stornieren"
}, },
"exercises": { "exercises": {
"heading": "Alle Übungen", "heading": "Alle Übungen",
@@ -797,7 +797,11 @@
"comments": { "comments": {
"title": "Comments", "title": "Comments",
"you": "You", "you": "You",
"placeholder": "Say something" "text_area_title": "Leave a comment",
"placeholder": "What do you think about this lesson?",
"comment_btn": "Comment",
"cancel_btn": "Cancel",
"load_more_btn": "Load more"
}, },
"polls": { "polls": {
@@ -1267,6 +1271,7 @@
"snackbar": { "snackbar": {
"success": "Success", "success": "Success",
"success_update": "Updated successfully", "success_update": "Updated successfully",
"success_delete": "Deleted successfully",
"something": "Something went wrong - Please try later", "something": "Something went wrong - Please try later",
"update": "Update", "update": "Update",
@@ -160,7 +160,8 @@
"generic": { "generic": {
"success_delete": "Borrado exitosamente", "success_delete": "Borrado exitosamente",
"try_again": "Error: inténtelo de nuevo más tarde" "try_again": "Error: inténtelo de nuevo más tarde"
} },
"success_delete": "Eliminado exitosamente"
}, },
"exercises": { "exercises": {
"heading": "Ejercicios", "heading": "Ejercicios",
@@ -574,7 +575,11 @@
"comments": { "comments": {
"title": "Comentarios", "title": "Comentarios",
"you": "Tú", "you": "Tú",
"placeholder": "Di algo" "placeholder": "Di algo",
"text_area_title": "Deja un comentario",
"comment_btn": "Comentario",
"load_more_btn": "Cargar más",
"cancel_btn": "Cancelar"
}, },
"exercises": { "exercises": {
"heading": "Todos los ejercicios", "heading": "Todos los ejercicios",
@@ -160,7 +160,8 @@
"generic": { "generic": {
"success_delete": "Supprimé avec succès", "success_delete": "Supprimé avec succès",
"try_again": "Erreur - Veuillez réessayer plus tard" "try_again": "Erreur - Veuillez réessayer plus tard"
} },
"success_delete": "Supprimé avec succès"
}, },
"exercises": { "exercises": {
"heading": "Des exercices", "heading": "Des exercices",
@@ -574,7 +575,11 @@
"comments": { "comments": {
"title": "commentaires", "title": "commentaires",
"you": "Toi", "you": "Toi",
"placeholder": "Dire quelque chose" "placeholder": "Dire quelque chose",
"text_area_title": "Laisser un commentaire",
"comment_btn": "Commentaire",
"load_more_btn": "Charger plus",
"cancel_btn": "Annuler"
}, },
"exercises": { "exercises": {
"heading": "Tous les exercices", "heading": "Tous les exercices",
@@ -160,7 +160,8 @@
"generic": { "generic": {
"success_delete": "सफलतापूर्वक मिटाया गया", "success_delete": "सफलतापूर्वक मिटाया गया",
"try_again": "त्रुटि - कृपया बाद में पुनः प्रयास करें" "try_again": "त्रुटि - कृपया बाद में पुनः प्रयास करें"
} },
"success_delete": "सफलतापूर्वक मिटाया गया"
}, },
"exercises": { "exercises": {
"heading": "अभ्यास", "heading": "अभ्यास",
@@ -574,7 +575,11 @@
"comments": { "comments": {
"title": "टिप्पणियाँ", "title": "टिप्पणियाँ",
"you": "आप", "you": "आप",
"placeholder": "कुछ कहो" "placeholder": "कुछ कहो",
"text_area_title": "एक टिप्पणी छोड़ें",
"comment_btn": "टिप्पणी",
"load_more_btn": "और लोड करें",
"cancel_btn": "रद्द करना"
}, },
"exercises": { "exercises": {
"heading": "सभी व्यायाम", "heading": "सभी व्यायाम",
@@ -160,7 +160,8 @@
"generic": { "generic": {
"success_delete": "Apagado com sucesso", "success_delete": "Apagado com sucesso",
"try_again": "Erro - Tente novamente mais tarde" "try_again": "Erro - Tente novamente mais tarde"
} },
"success_delete": "Excluído com sucesso"
}, },
"exercises": { "exercises": {
"heading": "Exercícios", "heading": "Exercícios",
@@ -574,7 +575,11 @@
"comments": { "comments": {
"title": "Comentários", "title": "Comentários",
"you": "Você", "you": "Você",
"placeholder": "Dizer algo" "placeholder": "Dizer algo",
"text_area_title": "Deixe um comentário",
"comment_btn": "Comentário",
"load_more_btn": "Carregar mais",
"cancel_btn": "Cancelar"
}, },
"exercises": { "exercises": {
"heading": "Todos os exercícios", "heading": "Todos os exercícios",
@@ -160,7 +160,8 @@
"generic": { "generic": {
"success_delete": "Удален успешно", "success_delete": "Удален успешно",
"try_again": "Ошибка. Повторите попытку позже." "try_again": "Ошибка. Повторите попытку позже."
} },
"success_delete": "Удален успешно"
}, },
"exercises": { "exercises": {
"heading": "Упражнения", "heading": "Упражнения",
@@ -574,7 +575,11 @@
"comments": { "comments": {
"title": "Комментарии", "title": "Комментарии",
"you": "Ты", "you": "Ты",
"placeholder": "Скажите что-то" "placeholder": "Скажите что-то",
"text_area_title": "Оставить комментарий",
"comment_btn": "Комментарий",
"load_more_btn": "Загрузить больше",
"cancel_btn": "Отмена"
}, },
"exercises": { "exercises": {
"heading": "Все упражнения", "heading": "Все упражнения",
@@ -160,7 +160,8 @@
"generic": { "generic": {
"success_delete": "Đã xoá thành công", "success_delete": "Đã xoá thành công",
"try_again": "Lỗi - Vui lòng thử lại sau" "try_again": "Lỗi - Vui lòng thử lại sau"
} },
"success_delete": "Đã xóa thành công"
}, },
"exercises": { "exercises": {
"heading": "Bài tập", "heading": "Bài tập",
@@ -574,7 +575,11 @@
"comments": { "comments": {
"title": "Bình luận", "title": "Bình luận",
"you": "Bạn", "you": "Bạn",
"placeholder": "Nói gì đó đi" "placeholder": "Nói gì đó đi",
"text_area_title": "Để lại một bình luận",
"comment_btn": "Bình luận",
"load_more_btn": "Tải thêm",
"cancel_btn": "Hủy bỏ"
}, },
"exercises": { "exercises": {
"heading": "Tất cả các bài tập", "heading": "Tất cả các bài tập",
+3 -1
View File
@@ -117,10 +117,12 @@ export interface LessonCommentInsertPayload {
comment: string; comment: string;
} }
export interface LessonComment { export interface LessonComment {
id: number;
name: string; name: string;
avatar: string; avatar: string;
comment: string; comment: string;
commentAt: string | Date; commentAt: string | Date;
groupmember_id: string;
} }
// Generated from https://supabase-schema.vercel.app/ // Generated from https://supabase-schema.vercel.app/
@@ -417,7 +419,7 @@ export interface Review {
description: string; description: string;
} }
interface Tabs { export interface Tabs {
id: number; id: number;
name: string; name: string;
} }
+2 -2
View File
@@ -10,7 +10,7 @@
import LMSNavigation from '$lib/components/Navigation/lms.svelte'; import LMSNavigation from '$lib/components/Navigation/lms.svelte';
import OrgLandingPage from '$lib/components/Org/LandingPage/index.svelte'; import OrgLandingPage from '$lib/components/Org/LandingPage/index.svelte';
import PlayQuiz from '$lib/components/Org/Quiz/Play/index.svelte'; import PlayQuiz from '$lib/components/Org/Quiz/Play/index.svelte';
import Restricted from '$lib/components/Page/Restricted.svelte'; import { PageRestricted } from '$lib/components/Page';
import PageLoadProgressBar from '$lib/components/Progress/PageLoadProgressBar.svelte'; import PageLoadProgressBar from '$lib/components/Progress/PageLoadProgressBar.svelte';
import Snackbar from '$lib/components/Snackbar/index.svelte'; import Snackbar from '$lib/components/Snackbar/index.svelte';
import UpgradeModal from '$lib/components/Upgrade/Modal.svelte'; import UpgradeModal from '$lib/components/Upgrade/Modal.svelte';
@@ -129,7 +129,7 @@
<Snackbar /> <Snackbar />
{#if data.org?.is_restricted || $currentOrg.is_restricted} {#if data.org?.is_restricted || $currentOrg.is_restricted}
<Restricted /> <PageRestricted />
{:else if data.skipAuth} {:else if data.skipAuth}
<PlayQuiz /> <PlayQuiz />
{:else if data.isOrgSite && !path} {:else if data.isOrgSite && !path}
+2 -2
View File
@@ -1,10 +1,10 @@
<script> <script>
import { page } from '$app/stores'; import { page } from '$app/stores';
import NotFound from '$lib/components/NotFound/NotFound.svelte'; import { PageNotFound } from '$lib/components/Page';
$: query = new URLSearchParams($page.url.search); $: query = new URLSearchParams($page.url.search);
$: isOrg = query.get('type') === 'org'; $: isOrg = query.get('type') === 'org';
</script> </script>
<NotFound {isOrg} /> <PageNotFound {isOrg} />
@@ -1,39 +1,37 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import PinFilled from 'carbon-icons-svelte/lib/PinFilled.svelte'; import Box from '$lib/components/Box/index.svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte';
import { group } from '$lib/components/Course/store';
import { profile } from '$lib/utils/store/user';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import { isNewFeedModal } from '$lib/components/Course/components/NewsFeed/store';
import NewsFeedCard from '$lib/components/Course/components/NewsFeed/NewsFeedCard.svelte';
import NewFeedModal from '$lib/components/Course/components/NewsFeed/NewFeedModal.svelte'; import NewFeedModal from '$lib/components/Course/components/NewsFeed/NewFeedModal.svelte';
import NewsFeedCard from '$lib/components/Course/components/NewsFeed/NewsFeedCard.svelte';
import NewsFeedLoader from '$lib/components/Course/components/NewsFeed/NewsFeedLoader.svelte';
import { isNewFeedModal, newsFeed } from '$lib/components/Course/components/NewsFeed/store';
import { group } from '$lib/components/Course/store';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import { PageBody, PageNav } from '$lib/components/Page';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import { snackbar } from '$lib/components/Snackbar/store';
import { supabase } from '$lib/utils/functions/supabase';
import { t } from '$lib/utils/functions/translations';
import { import {
createComment, createComment,
deleteNewsFeedComment,
deleteNewsFeed, deleteNewsFeed,
deleteNewsFeedComment,
fetchNewsFeedReaction,
fetchNewsFeeds, fetchNewsFeeds,
handleEditFeed, handleEditFeed,
toggleFeedIsPinned, toggleFeedIsPinned
fetchNewsFeedReaction
} from '$lib/utils/services/newsfeed'; } from '$lib/utils/services/newsfeed';
import { newsFeed } from '$lib/components/Course/components/NewsFeed/store';
import Box from '$lib/components/Box/index.svelte';
import { supabase } from '$lib/utils/functions/supabase';
import { snackbar } from '$lib/components/Snackbar/store';
import type { Feed } from '$lib/utils/types/feed';
import { import {
NOTIFICATION_NAME, NOTIFICATION_NAME,
triggerSendEmail triggerSendEmail
} from '$lib/utils/services/notification/notification'; } from '$lib/utils/services/notification/notification';
import NewsFeedLoader from '$lib/components/Course/components/NewsFeed/NewsFeedLoader.svelte';
import { t } from '$lib/utils/functions/translations';
import { currentOrg } from '$lib/utils/store/org'; import { currentOrg } from '$lib/utils/store/org';
import { profile } from '$lib/utils/store/user';
import type { Feed } from '$lib/utils/types/feed';
import type { CurrentOrg } from '$lib/utils/types/org'; import type { CurrentOrg } from '$lib/utils/types/org';
import { goto } from '$app/navigation'; import PinFilled from 'carbon-icons-svelte/lib/PinFilled.svelte';
export let data; export let data;
@@ -280,10 +278,10 @@
</div> </div>
{:else if !$newsFeed.length} {:else if !$newsFeed.length}
<Box> <Box>
<div class="flex justify-between flex-col items-center w-[90%] md:w-96"> <div class="flex w-[90%] flex-col items-center justify-between md:w-96">
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="my-2.5 mx-auto" /> <img src="/images/empty-lesson-icon.svg" alt="Lesson" class="mx-auto my-2.5" />
<h2 class="text-xl my-1.5 font-normal">{$t('course.navItem.news_feed.body_header')}</h2> <h2 class="my-1.5 text-xl font-normal">{$t('course.navItem.news_feed.body_header')}</h2>
<p class="text-sm text-center text-slate-500"> <p class="text-center text-sm text-slate-500">
{$t('course.navItem.news_feed.body_content')} {$t('course.navItem.news_feed.body_content')}
</p> </p>
</div> </div>
@@ -291,7 +289,7 @@
{:else} {:else}
{#each $newsFeed as feed} {#each $newsFeed as feed}
{#if feed.isPinned} {#if feed.isPinned}
<div class="flex items-center gap-2 mb-3"> <div class="mb-3 flex items-center gap-2">
<PinFilled size={16} /> <PinFilled size={16} />
<p class="text-sm">{$t('course.navItem.news_feed.pinned')}</p> <p class="text-sm">{$t('course.navItem.news_feed.pinned')}</p>
@@ -1,31 +1,30 @@
<script lang="ts"> <script lang="ts">
import { browser } from '$app/environment';
import Box from '$lib/components/Box/index.svelte';
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
import { getLectureNo } from '$lib/components/Course/function.js';
import { course, group } from '$lib/components/Course/store';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import { PageBody, PageNav } from '$lib/components/Page';
import { snackbar } from '$lib/components/Snackbar/store';
import { ROLE } from '$lib/utils/constants/roles';
import { t } from '$lib/utils/functions/translations';
import { takeAttendance } from '$lib/utils/services/attendance';
import { globalStore } from '$lib/utils/store/app';
import { attendance } from '$lib/utils/store/attendance';
import { profile } from '$lib/utils/store/user';
import type { GroupPerson, Lesson } from '$lib/utils/types/index';
import { import {
Search,
Checkbox, Checkbox,
Pagination,
Search,
StructuredList, StructuredList,
StructuredListBody, StructuredListBody,
StructuredListCell, StructuredListCell,
StructuredListHead, StructuredListHead,
StructuredListRow, StructuredListRow
Pagination
} from 'carbon-components-svelte'; } from 'carbon-components-svelte';
import AudioConsoleIcon from 'carbon-icons-svelte/lib/AudioConsole.svelte'; import AudioConsoleIcon from 'carbon-icons-svelte/lib/AudioConsole.svelte';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte';
import Box from '$lib/components/Box/index.svelte';
import { course, group } from '$lib/components/Course/store';
import { getLectureNo } from '$lib/components/Course/function.js';
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
import { ROLE } from '$lib/utils/constants/roles';
import { takeAttendance } from '$lib/utils/services/attendance';
import { snackbar } from '$lib/components/Snackbar/store';
import { attendance } from '$lib/utils/store/attendance';
import { profile } from '$lib/utils/store/user';
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';
export let data; export let data;
@@ -140,12 +139,12 @@
<CourseContainer bind:courseId={data.courseId}> <CourseContainer bind:courseId={data.courseId}>
<PageNav title={$t('course.navItem.attendance.title')} /> <PageNav title={$t('course.navItem.attendance.title')} />
<PageBody width="w-full max-w-6xl md:w-11/12"> <PageBody width="w-full max-w-6xl md:w-11/12">
<section class="flex items-center mx-2 lg:mx-9 my-5"> <section class="mx-2 my-5 flex items-center lg:mx-9">
<div <div
class="flex flex-col lg:flex-row items-start gap-2 lg:items-center justify-between w-full" class="flex w-full flex-col items-start justify-between gap-2 lg:flex-row lg:items-center"
> >
<div class="flex"> <div class="flex">
<p class="flex items-center mr-5"> <p class="mr-5 flex items-center">
<Checkbox checked disabled /> <Checkbox checked disabled />
{$t('course.navItem.attendance.present')} {$t('course.navItem.attendance.present')}
</p> </p>
@@ -164,8 +163,8 @@
</div> </div>
</section> </section>
<section class="my-5 mx-2 lg:mx-9"> <section class="mx-2 my-5 lg:mx-9">
<StructuredList class="m-0 relative"> <StructuredList class="relative m-0">
<!-- Moved the lesson headers outside the students loop --> <!-- Moved the lesson headers outside the students loop -->
<StructuredListHead class="bg-primary-100"> <StructuredListHead class="bg-primary-100">
<StructuredListRow head class="mx-7"> <StructuredListRow head class="mx-7">
@@ -186,8 +185,8 @@
<StructuredListBody> <StructuredListBody>
<StructuredListRow> <StructuredListRow>
<StructuredListCell> <StructuredListCell>
<div class="w-1/4 flex items-center"> <div class="flex w-1/4 items-center">
<p class="dark:text-white font-semibold"> <p class="font-semibold dark:text-white">
{student.profile.fullname} {student.profile.fullname}
</p> </p>
</div> </div>
@@ -213,7 +212,7 @@
{#if students.length === 0} {#if students.length === 0}
<Box className="h-[300px] w-full"> <Box className="h-[300px] w-full">
<AudioConsoleIcon size={32} class="carbon-icon w-80" /> <AudioConsoleIcon size={32} class="carbon-icon w-80" />
<h3 class="text-3xl text-gray-500 dark:text-white text-center"> <h3 class="text-center text-3xl text-gray-500 dark:text-white">
{$t('course.navItem.attendance.no_student')} {$t('course.navItem.attendance.no_student')}
</h3> </h3>
</Box> </Box>
@@ -1,9 +1,8 @@
<script lang="ts"> <script lang="ts">
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import IssueCertificateModal from '$lib/components/Course/components/Ceritficate/IssueCertificateModal.svelte'; import IssueCertificateModal from '$lib/components/Course/components/Ceritficate/IssueCertificateModal.svelte';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import { PageBody, PageNav } from '$lib/components/Page';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
// import DeleteConfirmation from '$lib/components/Course/components/People/DeleteConfirmation.svelte'; // import DeleteConfirmation from '$lib/components/Course/components/People/DeleteConfirmation.svelte';
import Design from '$lib/components/Course/components/Ceritficate/Design.svelte'; import Design from '$lib/components/Course/components/Ceritficate/Design.svelte';
// import Reports from '$lib/components/Course/components/Ceritficate/Reports.svelte'; // import Reports from '$lib/components/Course/components/Ceritficate/Reports.svelte';
@@ -1,29 +1,28 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores';
import PageNav from '$lib/components/PageNav/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import Box from '$lib/components/Box/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import {
lessons,
handleDelete,
lessonSections
} from '$lib/components/Course/components/Lesson/store/lessons';
import PageBody from '$lib/components/PageBody/index.svelte';
import DeleteLessonConfirmation from '$lib/components/Course/components/Lesson/DeleteLessonConfirmation.svelte';
import { handleAddLessonWidget } from '$lib/components/Course/components/Lesson/store';
import { course } from '$lib/components/Course/store';
import type { Lesson } from '$lib/utils/types';
import { t } from '$lib/utils/functions/translations';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { COURSE_VERSION } from '$lib/utils/types'; import { page } from '$app/stores';
import { profile } from '$lib/utils/store/user'; import Box from '$lib/components/Box/index.svelte';
import ActivateSectionsModal from '$lib/components/Course/components/Lesson/ActivateSectionsModal.svelte';
import DeleteLessonConfirmation from '$lib/components/Course/components/Lesson/DeleteLessonConfirmation.svelte';
import LessonList from '$lib/components/Course/components/Lesson/LessonList.svelte'; import LessonList from '$lib/components/Course/components/Lesson/LessonList.svelte';
import LessonSectionList from '$lib/components/Course/components/Lesson/LessonSectionList.svelte'; import LessonSectionList from '$lib/components/Course/components/Lesson/LessonSectionList.svelte';
import NewLessonModal from '$lib/components/Course/components/Lesson/NewLessonModal.svelte'; import NewLessonModal from '$lib/components/Course/components/Lesson/NewLessonModal.svelte';
import ActivateSectionsModal from '$lib/components/Course/components/Lesson/ActivateSectionsModal.svelte'; import { handleAddLessonWidget } from '$lib/components/Course/components/Lesson/store';
import {
handleDelete,
lessons,
lessonSections
} from '$lib/components/Course/components/Lesson/store/lessons';
import { course } from '$lib/components/Course/store';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import { PageBody, PageNav } from '$lib/components/Page';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import { t } from '$lib/utils/functions/translations';
import { profile } from '$lib/utils/store/user';
import type { Lesson } from '$lib/utils/types';
import { COURSE_VERSION } from '$lib/utils/types';
export let data; export let data;
@@ -135,7 +134,7 @@
</Box> </Box>
{:else if lessonsLength > 0} {:else if lessonsLength > 0}
{#if reorder} {#if reorder}
<p class="text-gray-400 text-center dark:text-white italic text-xs"> <p class="text-center text-xs italic text-gray-400 dark:text-white">
{$t('course.navItem.lessons.drag')} {$t('course.navItem.lessons.drag')}
</p> </p>
{/if} {/if}
@@ -147,10 +146,10 @@
{/if} {/if}
{:else} {:else}
<Box className="w-full lg:w-11/12 lg:px-4 m-auto"> <Box className="w-full lg:w-11/12 lg:px-4 m-auto">
<div class="flex justify-between flex-col items-center"> <div class="flex flex-col items-center justify-between">
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="my-2.5 mx-auto" /> <img src="/images/empty-lesson-icon.svg" alt="Lesson" class="mx-auto my-2.5" />
<h2 class="text-xl my-1.5 font-normal">{$t('course.navItem.lessons.body_header')}</h2> <h2 class="my-1.5 text-xl font-normal">{$t('course.navItem.lessons.body_header')}</h2>
<p class="text-sm text-center text-slate-500"> <p class="text-center text-sm text-slate-500">
{$t('course.navItem.lessons.body_content')} {$t('course.navItem.lessons.body_content')}
</p> </p>
</div> </div>
@@ -1,53 +1,50 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import { import {
checkExercisesComplete,
fetchLesson, fetchLesson,
updateLessonCompletion, updateLessonCompletion
checkExercisesComplete
} from '$lib/utils/services/courses'; } from '$lib/utils/services/courses';
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
import CheckmarkFilledIcon from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
import ListChecked from 'carbon-icons-svelte/lib/ListChecked.svelte';
import { globalStore } from '$lib/utils/store/app'; import { globalStore } from '$lib/utils/store/app';
import CheckmarkFilledIcon from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
import ListChecked from 'carbon-icons-svelte/lib/ListChecked.svelte';
import SendAlt from 'carbon-icons-svelte/lib/SendAlt.svelte'; import { browser } from '$app/environment';
import CourseIcon from '$lib/components/Icons/CourseIcon.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { Dropdown } 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';
import Materials from '$lib/components/Course/components/Lesson/Materials/index.svelte';
import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte';
import LessonVersionHistory from '$lib/components/Course/components/Lesson/LessonVersionHistory.svelte';
import MODES from '$lib/utils/constants/mode';
import { group, course } from '$lib/components/Course/store';
import Download from 'carbon-icons-svelte/lib/Download.svelte';
import ResultOld from 'carbon-icons-svelte/lib/ResultOld.svelte';
import OverflowMenuVertical from 'carbon-icons-svelte/lib/OverflowMenuVertical.svelte';
import { apps } from '$lib/components/Apps/store'; import { apps } from '$lib/components/Apps/store';
import APPS_CONSTANTS from '$lib/components/Apps/constants'; import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte';
import IconButton from '$lib/components/IconButton/index.svelte'; import { getIsLessonComplete } from '$lib/components/Course/components/Lesson/functions';
import { getGroupMemberId } from '$lib/components/Course/function'; import LessonVersionHistory from '$lib/components/Course/components/Lesson/LessonVersionHistory.svelte';
import Materials from '$lib/components/Course/components/Lesson/Materials/index.svelte';
import { import {
lesson, lesson,
lessons,
lessonByTranslation, lessonByTranslation,
lessons,
lessonSections lessonSections
} from '$lib/components/Course/components/Lesson/store/lessons'; } from '$lib/components/Course/components/Lesson/store/lessons';
import { browser } from '$app/environment'; import { getGroupMemberId } from '$lib/components/Course/function';
import { currentOrg } from '$lib/utils/store/org'; import { course, group } from '$lib/components/Course/store';
import IconButton from '$lib/components/IconButton/index.svelte';
import CourseIcon from '$lib/components/Icons/CourseIcon.svelte';
import { PageBody, PageNav } from '$lib/components/Page';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
import { COURSE_VERSION, type LessonCompletion, type Lesson } from '$lib/utils/types'; import MODES from '$lib/utils/constants/mode';
import { profile } from '$lib/utils/store/user';
import { getIsLessonComplete } from '$lib/components/Course/components/Lesson/functions';
import { t } from '$lib/utils/functions/translations';
import { LANGUAGES } from '$lib/utils/constants/translation'; import { LANGUAGES } from '$lib/utils/constants/translation';
import { t } from '$lib/utils/functions/translations';
import { currentOrg } from '$lib/utils/store/org';
import { profile } from '$lib/utils/store/user';
import { COURSE_VERSION, type Lesson, type LessonCompletion } from '$lib/utils/types';
import { Dropdown } from 'carbon-components-svelte';
import { ChevronLeft, ChevronRight, Edit, Save } from 'carbon-icons-svelte'; import { ChevronLeft, ChevronRight, Edit, Save } from 'carbon-icons-svelte';
import Download from 'carbon-icons-svelte/lib/Download.svelte';
import OverflowMenuVertical from 'carbon-icons-svelte/lib/OverflowMenuVertical.svelte';
import ResultOld from 'carbon-icons-svelte/lib/ResultOld.svelte';
export let data; export let data;
@@ -73,7 +70,7 @@
async function fetchReqData(lessonId = '', isMaterialsTabActive: boolean) { async function fetchReqData(lessonId = '', isMaterialsTabActive: boolean) {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
$lesson.isFetching = true; $lesson.isFetching = true;
}, 1000); }, 500);
let lessonData; let lessonData;
if (isMaterialsTabActive) { if (isMaterialsTabActive) {
const lesson = await fetchLesson(lessonId); const lesson = await fetchLesson(lessonId);
@@ -84,9 +81,9 @@
console.log({ lessonData }); console.log({ lessonData });
const totalExercises = lessonData?.totalExercises?.[0] && lessonData.totalExercises[0].count; const totalExercises = lessonData?.totalExercises?.[0]?.count || 0;
const totalComments = lessonData?.totalComments?.[0] && lessonData.totalComments[0].count; const totalComments = lessonData?.totalComments?.[0]?.count || 0;
setLesson(lessonData, totalExercises || 0, totalComments || 0); setLesson(lessonData, totalExercises, totalComments);
$lesson.isFetching = false; $lesson.isFetching = false;
} }
@@ -321,7 +318,7 @@
$: path = $page.url?.pathname?.replace(/\/exercises[\/ 0-9 a-z -]*/, ''); $: path = $page.url?.pathname?.replace(/\/exercises[\/ 0-9 a-z -]*/, '');
$: if (data.courseId && browser) { $: if (data.courseId && $profile.id && browser) {
mode = MODES.view; mode = MODES.view;
fetchReqData(data.lessonId, data.isMaterialsTabActive); fetchReqData(data.lessonId, data.isMaterialsTabActive);
} }
@@ -414,10 +411,6 @@
{:else if !!data.lessonId} {:else if !!data.lessonId}
<PageBody <PageBody
bind:isPageNavHidden={$globalStore.isStudent} bind:isPageNavHidden={$globalStore.isStudent}
onClick={() => {
$apps.open = false;
$apps.dropdown = false;
}}
width="lg:w-full xl:w-11/12" width="lg:w-full xl:w-11/12"
className="overflow-x-hidden" className="overflow-x-hidden"
> >
@@ -465,14 +458,6 @@
<CourseIcon /> <CourseIcon />
</button> </button>
{/if} {/if}
<button
class="my-2 flex items-center border border-b-0 border-l-0 border-t-0 border-gray-300 px-2 pr-4 disabled:cursor-not-allowed disabled:opacity-10"
on:click={() => handleAppClick(APPS_CONSTANTS.APPS.COMMENTS)}
disabled={$globalStore.isStudent && !$currentOrg.customization.apps.comments}
>
<SendAlt size={24} class="carbon-icon" />
<span class="ml-1">{$lesson.totalComments}</span>
</button>
<button <button
class="my-2 flex items-center border border-b-0 border-l-0 border-t-0 border-gray-300 px-2 pr-4" class="my-2 flex items-center border border-b-0 border-l-0 border-t-0 border-gray-300 px-2 pr-4"
on:click={() => markLessonComplete(data.lessonId)} on:click={() => markLessonComplete(data.lessonId)}
@@ -1,31 +1,29 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation';
import AudioConsoleIcon from 'carbon-icons-svelte/lib/AudioConsole.svelte';
import Papa from 'papaparse';
import { OverflowMenu, OverflowMenuItem, Loading } from 'carbon-components-svelte';
import Download from 'carbon-icons-svelte/lib/Download.svelte';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import Box from '$lib/components/Box/index.svelte';
import { ROLE } from '$lib/utils/constants/roles';
import { group } from '$lib/components/Course/store';
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
import { fetchExercisesByMarks } from '$lib/utils/services/courses';
import { getLectureNo } from '$lib/components/Course/function.js';
import { fetchMarks } from '$lib/utils/services/marks';
import { profile } from '$lib/utils/store/user';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { course } from '$lib/components/Course/store'; import { goto } from '$app/navigation';
import Box from '$lib/components/Box/index.svelte';
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
import { getLectureNo } from '$lib/components/Course/function.js';
import { course, group } from '$lib/components/Course/store';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import { PageBody, PageNav } from '$lib/components/Page';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
import { globalStore } from '$lib/utils/store/app'; import { ROLE } from '$lib/utils/constants/roles';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
import { fetchExercisesByMarks } from '$lib/utils/services/courses';
import { fetchMarks } from '$lib/utils/services/marks';
import { globalStore } from '$lib/utils/store/app';
import { currentOrg } from '$lib/utils/store/org'; import { currentOrg } from '$lib/utils/store/org';
import type { CurrentOrg } from '$lib/utils/types/org'; import { profile } from '$lib/utils/store/user';
import type { GroupPerson } from '$lib/utils/types'; import type { GroupPerson } from '$lib/utils/types';
import type { CurrentOrg } from '$lib/utils/types/org';
import { Loading, OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
import AudioConsoleIcon from 'carbon-icons-svelte/lib/AudioConsole.svelte';
import Download from 'carbon-icons-svelte/lib/Download.svelte';
import jsPDF from 'jspdf'; import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable'; import autoTable from 'jspdf-autotable';
import Papa from 'papaparse';
export let data; export let data;
@@ -240,19 +238,19 @@
</PageNav> </PageNav>
<PageBody width="w-full max-w-6xl md:w-11/12"> <PageBody width="w-full max-w-6xl md:w-11/12">
<div id="tableContainer" class="table rounded-md border border-gray-300 w-full"> <div id="tableContainer" class="table w-full rounded-md border border-gray-300">
<div class="flex items-center {borderBottomGrey}"> <div class="flex items-center {borderBottomGrey}">
<div class="box flex items-center p-3"> <div class="box flex items-center p-3">
<p class="dark:text-white w-40">{$t('course.navItem.marks.student')}</p> <p class="w-40 dark:text-white">{$t('course.navItem.marks.student')}</p>
</div> </div>
{#each $lessons as lesson, index} {#each $lessons as lesson, index}
{#if lessonMapping[lesson.id]} {#if lessonMapping[lesson.id]}
<div class="box flex flex-col items-center {borderleftGrey}"> <div class="box flex flex-col items-center {borderleftGrey}">
<p class="dark:text-white col lesson-number" title={lesson.title}> <p class="col lesson-number dark:text-white" title={lesson.title}>
{getLectureNo(index + 1)} {getLectureNo(index + 1)}
</p> </p>
<div <div
class="flex h-full items-center border-r-0 border-t border-b-0 border-l-0 border-gray-300" class="flex h-full items-center border-b-0 border-l-0 border-r-0 border-t border-gray-300"
> >
{#each Object.keys(lessonMapping[lesson.id]) as exerciseId, index} {#each Object.keys(lessonMapping[lesson.id]) as exerciseId, index}
<p <p
@@ -268,20 +266,20 @@
{/if} {/if}
{/each} {/each}
<div class="box flex items-center {borderleftGrey}"> <div class="box flex items-center {borderleftGrey}">
<p class="dark:text-white w-20 text-center">{$t('course.navItem.marks.total')}</p> <p class="w-20 text-center dark:text-white">{$t('course.navItem.marks.total')}</p>
</div> </div>
</div> </div>
{#each students as student} {#each students as student}
<div class="flex relative items-center p-3 cursor-pointer {borderBottomGrey}"> <div class="relative flex cursor-pointer items-center p-3 {borderBottomGrey}">
<div class="w-40 flex items-center"> <div class="flex w-40 items-center">
<img <img
alt="Student avatar" alt="Student avatar"
src={student.profile.avatar_url} src={student.profile.avatar_url}
class="w-8 h-8 rounded-full mr-2" class="mr-2 h-8 w-8 rounded-full"
/> />
<div class="text-sm"> <div class="text-sm">
<p class="dark:text-white font-semibold"> <p class="font-semibold dark:text-white">
{student.profile.fullname} {student.profile.fullname}
</p> </p>
<p class="dark:text-white"> <p class="dark:text-white">
@@ -293,7 +291,7 @@
{#if lessonMapping[lesson.id]} {#if lessonMapping[lesson.id]}
<div class="flex items-center"> <div class="flex items-center">
{#each Object.keys(lessonMapping[lesson.id]) as exerciseId} {#each Object.keys(lessonMapping[lesson.id]) as exerciseId}
<p class="dark:text-white col"> <p class="col dark:text-white">
{studentMarksByExerciseId[student.id] {studentMarksByExerciseId[student.id]
? studentMarksByExerciseId[student.id][exerciseId] || '-' ? studentMarksByExerciseId[student.id][exerciseId] || '-'
: '-'} : '-'}
@@ -303,9 +301,9 @@
{/if} {/if}
{/each} {/each}
<div class="w-20 flex items-center"> <div class="flex w-20 items-center">
<div class="text-sm"> <div class="text-sm">
<p class="dark:text-white col"> <p class="col dark:text-white">
{calculateStudentTotal(studentMarksByExerciseId[student.id])} {calculateStudentTotal(studentMarksByExerciseId[student.id])}
</p> </p>
</div> </div>
@@ -3,8 +3,7 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import CourseContainer from '$lib/components/CourseContainer/index.svelte'; import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import IconButton from '$lib/components/IconButton/index.svelte'; import IconButton from '$lib/components/IconButton/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte'; import { PageBody, PageNav } from '$lib/components/Page';
import PageNav from '$lib/components/PageNav/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte'; import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
@@ -1,8 +1,7 @@
<script> <script>
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
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 Settings from '$lib/components/Course/components/Settings/index.svelte';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import { PageBody, PageNav } from '$lib/components/Page';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
export let data; export let data;
@@ -1,38 +1,37 @@
<script lang="ts"> <script lang="ts">
import { flip } from 'svelte/animate'; import { browser } from '$app/environment';
import { dndzone } from 'svelte-dnd-action';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { SkeletonPlaceholder } from 'carbon-components-svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import MarkExerciseModal from '$lib/components/Course/components/Lesson/Exercise/MarkExerciseModal.svelte';
import Chip from '$lib/components/Chip/index.svelte'; import Chip from '$lib/components/Chip/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte'; import MarkExerciseModal from '$lib/components/Course/components/Lesson/Exercise/MarkExerciseModal.svelte';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import { course } from '$lib/components/Course/store';
import {
fetchSubmissions,
updateSubmission,
updateQuestionAnswer,
deleteSubmission
} from '$lib/utils/services/submissions';
import { formatAnswers } from '$lib/components/Course/function.js'; import { formatAnswers } from '$lib/components/Course/function.js';
import { course } from '$lib/components/Course/store';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import { PageBody, PageNav } from '$lib/components/Page';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
import isSubmissionEarly from '$lib/utils/functions/isSubmissionEarly';
import formatDate from '$lib/utils/functions/formatDate'; import formatDate from '$lib/utils/functions/formatDate';
import { import isSubmissionEarly from '$lib/utils/functions/isSubmissionEarly';
triggerSendEmail,
NOTIFICATION_NAME
} 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'; import { t } from '$lib/utils/functions/translations';
import {
NOTIFICATION_NAME,
triggerSendEmail
} from '$lib/utils/services/notification/notification';
import {
deleteSubmission,
fetchSubmissions,
updateQuestionAnswer,
updateSubmission
} from '$lib/utils/services/submissions';
import { currentOrg, currentOrgDomain } from '$lib/utils/store/org';
import type { import type {
SubmissionIdData, SubmissionIdData,
SubmissionItem, SubmissionItem,
SubmissionSection SubmissionSection
} from '$lib/utils/types/submission'; } from '$lib/utils/types/submission';
import { SkeletonPlaceholder } from 'carbon-components-svelte';
import { dndzone } from 'svelte-dnd-action';
import { flip } from 'svelte/animate';
export let data; export let data;
const { courseId } = data; const { courseId } = data;
@@ -373,23 +372,23 @@
<PageNav title={$t('course.navItem.submissions.title')} /> <PageNav title={$t('course.navItem.submissions.title')} />
<PageBody width="w-full max-w-6xl md:w-11/12 overflow-x-auto"> <PageBody width="w-full max-w-6xl md:w-11/12 overflow-x-auto">
<div class="flex items-center w-full"> <div class="flex w-full items-center">
{#each sections as { id, title, items }, idx (id)} {#each sections as { id, title, items }, idx (id)}
<div <div
class="section rounded-md bg-gray-100 dark:bg-black border border-gray-50 dark:border-neutral-600 p-3 h-80 mr-3 overflow-hidden" class="section mr-3 h-80 overflow-hidden rounded-md border border-gray-50 bg-gray-100 p-3 dark:border-neutral-600 dark:bg-black"
animate:flip={{ duration: flipDurationMs }} animate:flip={{ duration: flipDurationMs }}
> >
<div class="flex items-center mb-2"> <div class="mb-2 flex items-center">
<Chip value={items.length} className="bg-set dark:bg-neutral-800" /> <Chip value={items.length} className="bg-set dark:bg-neutral-800" />
<p class="dark:text-white ml-2 font-bold">{title}</p> <p class="ml-2 font-bold dark:text-white">{title}</p>
</div> </div>
{#if fetching} {#if fetching}
<SkeletonPlaceholder style="width: 100%; height: 170px;" class="rounded-md my-2" /> <SkeletonPlaceholder style="width: 100%; height: 170px;" class="my-2 rounded-md" />
<SkeletonPlaceholder style="width: 100%; height: 170px;" class="rounded-md my-2" /> <SkeletonPlaceholder style="width: 100%; height: 170px;" class="my-2 rounded-md" />
<SkeletonPlaceholder style="width: 100%; height: 170px;" class="rounded-md my-2" /> <SkeletonPlaceholder style="width: 100%; height: 170px;" class="my-2 rounded-md" />
{:else} {:else}
<div <div
class="content pr-2 overflow-y-auto mb-3" class="content mb-3 overflow-y-auto pr-2"
use:dndzone={{ use:dndzone={{
items, items,
flipDurationMs, flipDurationMs,
@@ -402,19 +401,19 @@
<div <div
class="{item.isEarly class="{item.isEarly
? 'border-none' ? 'border-none'
: 'border border-red-700'} w-full my-2 mx-0 rounded-md bg-white dark:bg-neutral-800 py-3 px-3" : 'border border-red-700'} mx-0 my-2 w-full rounded-md bg-white px-3 py-3 dark:bg-neutral-800"
animate:flip={{ duration: flipDurationMs }} animate:flip={{ duration: flipDurationMs }}
> >
<a <a
class="flex w-full items-center cursor-pointer text-black mb-2" class="mb-2 flex w-full cursor-pointer items-center text-black"
href={`${$page.url.pathname}?submissionId=${item.id}`} href={`${$page.url.pathname}?submissionId=${item.id}`}
> >
<img <img
alt="Student avatar" alt="Student avatar"
class="block rounded-full h-6 w-6" class="block h-6 w-6 rounded-full"
src={item.student.avatar_url} src={item.student.avatar_url}
/> />
<p class="dark:text-white ml-2 text-sm"> <p class="ml-2 text-sm dark:text-white">
{item.student.username} {item.student.username}
</p> </p>
</a> </a>
@@ -425,15 +424,15 @@
{item.exercise.title} {item.exercise.title}
</a> </a>
<a <a
class="flex items-center no-underline hover:underline text-black my-2" class="my-2 flex items-center text-black no-underline hover:underline"
href="{$page.url?.pathname?.replace('submissions', 'lessons')}/{item.lesson href="{$page.url?.pathname?.replace('submissions', 'lessons')}/{item.lesson
.id}/exercises/{item.exercise.id}" .id}/exercises/{item.exercise.id}"
> >
<p class="dark:text-white text-grey text-sm"> <p class="text-grey text-sm dark:text-white">
#{item.lesson.title} #{item.lesson.title}
</p> </p>
</a> </a>
<p class="dark:text-white text-gray-500 text-xs"> <p class="text-xs text-gray-500 dark:text-white">
{item.submittedAt} {item.submittedAt}
</p> </p>
<!-- <div class="badge rounded-md px-2 bg-green-500 text-white"> <!-- <div class="badge rounded-md px-2 bg-green-500 text-white">