feat: setup drizzle (#594)
* chore: migration to SvelteKit 2 * stage 1: migrate to svelte 5 and fix rerendering issues * stage 2: fix more $: reactivity across dashboard * fix merge conflicts * chore: migrate code in main to svelte 5 * chore: update preventDefault * chore: simplify tsconfig and remove unused mobileMenuRef in MobileMenu.svelte * chore: enforce .ts across dashboard * rename from classroomio-com to website * stage-3: Replace carbon icon svelte with lucide icon * update lock file * chore: finish icon replacement * fix: settings and setup page multiple rerender * stage 3: More icon and rerendering fixes * feat: setup drizzle and better auth * fix: onboarding validation error showing translation key #586 (#593) * fix: onboarding validation error showing translation key #586 * fix: remove debug log for profile update errors * chore: add a clean command * fix: remove message from email verification response
This commit is contained in:
@@ -1,45 +1,42 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import ErrorIcon from '$lib/components/Icons/ErrorIcon.svelte';
|
||||
import ErrorIconDarkMode from '$lib/components/Icons/ErrorIconDarkMode.svelte';
|
||||
|
||||
if ($page.status == 404 && browser) {
|
||||
if (page.status == 404 && browser) {
|
||||
goto('/404');
|
||||
}
|
||||
console.error($page.error?.message);
|
||||
console.error(page.error?.message);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$page.status}</title>
|
||||
<title>{page.status}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
class="error-container m-auto flex flex-col-reverse lg:flex-row md:flex-col-reverse items-center justify-center h-full py-10 dark:bg-black"
|
||||
class="error-container m-auto flex h-full flex-col-reverse items-center justify-center py-10 md:flex-col-reverse lg:flex-row dark:bg-black"
|
||||
>
|
||||
<div class="md:w-full p-2 lg:w-2/4">
|
||||
<h2 class="w-full text-lg lg:text-3xl font-medium lg:font-normal">
|
||||
Something unexpected occured.
|
||||
</h2>
|
||||
<p class="dark:text-white w-full text-base font-normal my-6 text-gray-600">
|
||||
Don't worry, your learning is safe. It isn't your fault, it is ours. We have gotten the error
|
||||
notification and will push a fix ASAP. In the meantime, take a short break and come back a bit
|
||||
later.
|
||||
<div class="p-2 md:w-full lg:w-2/4">
|
||||
<h2 class="w-full text-lg font-medium lg:text-3xl lg:font-normal">Something unexpected occured.</h2>
|
||||
<p class="my-6 w-full text-base font-normal text-gray-600 dark:text-white">
|
||||
Don't worry, your learning is safe. It isn't your fault, it is ours. We have gotten the error notification and
|
||||
will push a fix ASAP. In the meantime, take a short break and come back a bit later.
|
||||
</p>
|
||||
<a
|
||||
class="bg-primary-700 hover:bg-primary-900 hover:no-underline transition-all py-3 px-9 rounded-xl text-white mt-3"
|
||||
class="bg-primary-700 hover:bg-primary-900 mt-3 rounded-xl px-9 py-3 text-white transition-all hover:no-underline"
|
||||
href="/">Go Home</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="w-72 lg:w-2/4 flex items-center justify-center">
|
||||
<div class="flex w-72 items-center justify-center lg:w-2/4">
|
||||
<!-- to check if it's in dark mode so as to render the appropriate svg -->
|
||||
{#if $globalStore.isDark}
|
||||
<ErrorIconDarkMode />
|
||||
<ErrorIcon size="{16}DarkMode" />
|
||||
{:else}
|
||||
<ErrorIcon />
|
||||
<ErrorIcon size={16} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,7 +109,7 @@ export const load = async ({ url, cookies, request }): Promise<LoadOutput> => {
|
||||
response.org = (await getCurrentOrg(response.orgSiteName, true)) || null;
|
||||
|
||||
if (!response.org && !isDev) {
|
||||
throw redirect(307, 'https://app.classroomio.com/404?type=org');
|
||||
redirect(307, 'https://app.classroomio.com/404?type=org');
|
||||
} else if (!response.org && _orgSiteName) {
|
||||
cookies.delete('_orgSiteName', { path: '/' });
|
||||
}
|
||||
@@ -117,7 +117,7 @@ export const load = async ({ url, cookies, request }): Promise<LoadOutput> => {
|
||||
response.skipAuth = true;
|
||||
} else if (!APP_SUBDOMAINS.includes(subdomain) && !isDev) {
|
||||
// This case is for anything in our blockedSubdomains
|
||||
throw redirect(307, 'https://app.classroomio.com');
|
||||
redirect(307, 'https://app.classroomio.com');
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -128,9 +128,7 @@ function isURLCustomDomain(url: URL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const notCustomDomainHosts = [env.PRIVATE_APP_HOST || '', 'classroomio.com', 'vercel.app'].filter(
|
||||
Boolean
|
||||
);
|
||||
const notCustomDomainHosts = [env.PRIVATE_APP_HOST || '', 'classroomio.com', 'vercel.app'].filter(Boolean);
|
||||
|
||||
return !notCustomDomainHosts.some((host) => url.host.endsWith(host));
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { page } from '$app/stores';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import Apps from '$lib/components/Apps/index.svelte';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import OrgNavigation from '$lib/components/Navigation/app.svelte';
|
||||
import LandingNavigation from '$lib/components/Navigation/index.svelte';
|
||||
@@ -11,13 +10,12 @@
|
||||
import OrgLandingPage from '$lib/components/Org/LandingPage/index.svelte';
|
||||
import PlayQuiz from '$lib/components/Org/Quiz/Play/index.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 UpgradeModal from '$lib/components/Upgrade/Modal.svelte';
|
||||
import { isCoursesPage, isLMSPage, isOrgPage, toggleBodyByMode } from '$lib/utils/functions/app';
|
||||
import { getProfile, setupAnalytics } from '$lib/utils/functions/appSetup';
|
||||
import hideNavByRoute from '$lib/utils/functions/routes/hideNavByRoute';
|
||||
import showAppsSideBar from '$lib/utils/functions/showAppsSideBar';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import { setTheme } from '$lib/utils/functions/theme';
|
||||
import { initOrgAnalytics } from '$lib/utils/services/posthog';
|
||||
@@ -32,12 +30,12 @@
|
||||
|
||||
import '../app.postcss';
|
||||
|
||||
export let data;
|
||||
let { data, children } = $props();
|
||||
|
||||
let supabase = getSupabase();
|
||||
let path = $page.url?.pathname?.replace('/', '');
|
||||
let path = $derived($page.url?.pathname?.replace('/', ''));
|
||||
let queryParam = $page.url?.search;
|
||||
let carbonTheme: CarbonTheme = 'white';
|
||||
let carbonTheme: CarbonTheme = $derived($globalStore.isDark ? 'g100' : 'white');
|
||||
|
||||
function handleResize() {
|
||||
isMobile.update(() => window.innerWidth <= 760);
|
||||
@@ -70,7 +68,7 @@
|
||||
// return goto('/login?redirect=/' + path);
|
||||
// }
|
||||
|
||||
const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => {
|
||||
const { data: authListener } = supabase.auth.onAuthStateChange((event) => {
|
||||
// Log key events
|
||||
console.log(`event`, event);
|
||||
|
||||
@@ -113,12 +111,10 @@
|
||||
};
|
||||
});
|
||||
|
||||
$: path = $page.url?.pathname?.replace('/', '');
|
||||
$: carbonTheme = $globalStore.isDark ? 'g100' : 'white';
|
||||
$: metaTags = merge(data.baseMetaTags, $page.data.pageMetaTags);
|
||||
let metaTags = $derived(merge(data.baseMetaTags, $page.data.pageMetaTags));
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={handleResize} />
|
||||
<svelte:window onresize={handleResize} />
|
||||
|
||||
<MetaTags {...metaTags} />
|
||||
|
||||
@@ -150,15 +146,11 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<PageLoadProgressBar textColorClass="text-neutral-700" />
|
||||
<!-- <PageLoadProgressBar textColorClass="text-neutral-700" /> -->
|
||||
{/if}
|
||||
|
||||
<div class={path.includes('home') ? '' : 'flex justify-between'}>
|
||||
<slot />
|
||||
|
||||
{#if showAppsSideBar(path)}
|
||||
<Apps />
|
||||
{/if}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</main>
|
||||
{/if}
|
||||
@@ -176,11 +168,7 @@
|
||||
--main-primary-color: rgba(29, 78, 216, 1);
|
||||
--border-color: #eaecef;
|
||||
--app-background-color: #fafbfc;
|
||||
--app-background: radial-gradient(
|
||||
circle at 10% 20%,
|
||||
rgb(239, 246, 249) 0%,
|
||||
rgb(206, 239, 253) 90%
|
||||
);
|
||||
--app-background: radial-gradient(circle at 10% 20%, rgb(239, 246, 249) 0%, rgb(206, 239, 253) 90%);
|
||||
--dark-app-background: radial-gradient(circle at 10% 20%, rgb(0 0 0) 0%, rgb(27 60 74) 90%);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
<script>
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import FlowStreamReferenceIcon from 'carbon-icons-svelte/lib/FlowStreamReference.svelte';
|
||||
import ChartClusterBarIcon from 'carbon-icons-svelte/lib/ChartClusterBar.svelte';
|
||||
import MachineLearningModelIcon from 'carbon-icons-svelte/lib/MachineLearningModel.svelte';
|
||||
import DoorOpenIcon from '@lucide/svelte/icons/door-open';
|
||||
import ChartAreaIcon from '@lucide/svelte/icons/chart-area';
|
||||
import BotIcon from '@lucide/svelte/icons/bot';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import { validateEmail } from '$lib/utils/functions/validateEmail';
|
||||
|
||||
let email = '';
|
||||
let isAdding = false;
|
||||
let success = false;
|
||||
let email = $state('');
|
||||
let isAdding = $state(false);
|
||||
let success = $state(false);
|
||||
|
||||
const supabase = getSupabase();
|
||||
const animate = 'transition delay-75 duration-300 ease-in-out';
|
||||
const areas = [
|
||||
{
|
||||
title: 'Access',
|
||||
description:
|
||||
'Quickly find any material you need to get your job done. ClassroomIO offers the All-in-on-platform'
|
||||
description: 'Quickly find any material you need to get your job done. ClassroomIO offers the All-in-on-platform'
|
||||
},
|
||||
{
|
||||
title: 'Analytics',
|
||||
@@ -56,7 +57,7 @@
|
||||
<title>ClassroomIO - The classroom software</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="md:h-[93vh] w-screen flex items-center justify-center flex-col m-2 sm:m-0 font-sans">
|
||||
<div class="m-2 flex w-screen flex-col items-center justify-center font-sans sm:m-0 md:h-[93vh]">
|
||||
<!--
|
||||
<img
|
||||
src="/logo-192.png"
|
||||
@@ -66,16 +67,14 @@
|
||||
/>
|
||||
-->
|
||||
<div>
|
||||
<h3 class="dark:text-white text-4xl text-center">
|
||||
<h3 class="text-center text-4xl dark:text-white">
|
||||
Classroom<span class="text-primary-700">IO</span>
|
||||
</h3>
|
||||
<p class="dark:text-white text-lg text-center">
|
||||
The operating system for classroooms of the future 🚀🚀🚀.
|
||||
</p>
|
||||
<p class="text-center text-lg dark:text-white">The operating system for classroooms of the future 🚀🚀🚀.</p>
|
||||
</div>
|
||||
|
||||
<form on:submit|preventDefault={handleSubmit} class="my-4 hidden">
|
||||
<div class="flex items-center flex-col sm:flex-row">
|
||||
<form onsubmit={preventDefault(handleSubmit)} class="my-4 hidden">
|
||||
<div class="flex flex-col items-center sm:flex-row">
|
||||
{#if success}
|
||||
<p class="dark:text-white">You have been added successfully. Thanks for joining.</p>
|
||||
{:else}
|
||||
@@ -92,18 +91,18 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="flex flex-col md:flex-row mt-4">
|
||||
<div class="mt-4 flex flex-col md:flex-row">
|
||||
{#each areas as area, index}
|
||||
<div
|
||||
class="max-w-[350px] m-3 bg-white dark:bg-black rounded-md py-3 px-12 active shadow-xl border-2 hover:border-primary-700 {animate}"
|
||||
class="active hover:border-primary-700 m-3 max-w-[350px] rounded-md border-2 bg-white px-12 py-3 shadow-xl dark:bg-black {animate}"
|
||||
>
|
||||
<h3 class="dark:text-white text-3xl">
|
||||
<h3 class="text-3xl dark:text-white">
|
||||
{#if index === 0}
|
||||
<FlowStreamReferenceIcon size={32} class="carbon-icon dark:text-white" />
|
||||
<DoorOpenIcon size={16} />
|
||||
{:else if index === 1}
|
||||
<ChartClusterBarIcon size={32} class="carbon-icon dark:text-white" />
|
||||
<ChartAreaIcon size={16} />
|
||||
{:else if index === 2}
|
||||
<MachineLearningModelIcon size={32} class="carbon-icon dark:text-white" />
|
||||
<BotIcon size={16} />
|
||||
{/if}
|
||||
{area.title}
|
||||
</h3>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
|
||||
import { PageNotFound } from '$lib/components/Page';
|
||||
|
||||
$: query = new URLSearchParams($page.url.search);
|
||||
$: isOrg = query.get('type') === 'org';
|
||||
let query = $derived(new URLSearchParams(page.url.search));
|
||||
let isOrg = $derived(query.get('type') === 'org');
|
||||
</script>
|
||||
|
||||
<PageNotFound {isOrg} />
|
||||
|
||||
@@ -79,9 +79,7 @@ async function getCourseAnalytics(courseId: string): Promise<CourseAnalytics> {
|
||||
|
||||
// Count tutors and students
|
||||
const members = (courseData.group as any)?.members || [];
|
||||
analytics.totalTutors = members.filter(
|
||||
(member) => member.role_id === 1 || member.role_id === 2
|
||||
).length;
|
||||
analytics.totalTutors = members.filter((member) => member.role_id === 1 || member.role_id === 2).length;
|
||||
analytics.totalStudents = members.filter((member) => member.role_id === 3).length;
|
||||
|
||||
// Count lessons and exercises
|
||||
@@ -91,35 +89,27 @@ async function getCourseAnalytics(courseId: string): Promise<CourseAnalytics> {
|
||||
|
||||
// Get student analytics
|
||||
const studentAnalytics = await Promise.all(
|
||||
members
|
||||
.filter((member) => member.role_id === 3)
|
||||
.map((member) => getStudentOverview(courseId, member))
|
||||
members.filter((member) => member.role_id === 3).map((member) => getStudentOverview(courseId, member))
|
||||
);
|
||||
|
||||
analytics.students = studentAnalytics.filter(
|
||||
(student): student is StudentOverview => student !== null
|
||||
);
|
||||
analytics.students = studentAnalytics.filter((student): student is StudentOverview => student !== null);
|
||||
|
||||
// Calculate aggregated metrics
|
||||
if (analytics.students.length > 0) {
|
||||
analytics.lessonCompletionRate = Math.round(
|
||||
analytics.students.reduce((sum, student) => sum + student.progressPercentage, 0) /
|
||||
analytics.students.length
|
||||
analytics.students.reduce((sum, student) => sum + student.progressPercentage, 0) / analytics.students.length
|
||||
);
|
||||
|
||||
analytics.exerciseCompletionRate = Math.round(
|
||||
analytics.students.reduce((sum, student) => {
|
||||
const completionRate =
|
||||
student.totalExercises > 0
|
||||
? (student.exercisesSubmitted / student.totalExercises) * 100
|
||||
: 0;
|
||||
student.totalExercises > 0 ? (student.exercisesSubmitted / student.totalExercises) * 100 : 0;
|
||||
return sum + completionRate;
|
||||
}, 0) / analytics.students.length
|
||||
);
|
||||
|
||||
analytics.averageGrade = Math.round(
|
||||
analytics.students.reduce((sum, student) => sum + student.averageGrade, 0) /
|
||||
analytics.students.length
|
||||
analytics.students.reduce((sum, student) => sum + student.averageGrade, 0) / analytics.students.length
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,8 +157,7 @@ async function getStudentOverview(courseId: string, member: any): Promise<Studen
|
||||
const userExercisesStats = await fetchUserExercisesStats(courseId, member.profile_id);
|
||||
|
||||
// Calculate exercise completion using the same logic as people page
|
||||
const completedExercises =
|
||||
userExercisesStats?.filter((exercise) => exercise.isCompleted)?.length || 0;
|
||||
const completedExercises = userExercisesStats?.filter((exercise) => exercise.isCompleted)?.length || 0;
|
||||
const totalExercises = userExercisesStats?.length || 0;
|
||||
|
||||
// Get last login using the same approach as analytics/user API
|
||||
@@ -197,10 +186,8 @@ async function getStudentOverview(courseId: string, member: any): Promise<Studen
|
||||
const progressPercentage = calcPercentageWithRounding(lessonsCompleted, totalLessons);
|
||||
|
||||
// Calculate average grade using the same logic as the people page
|
||||
const totalEarnedPoints =
|
||||
userExercisesStats?.reduce((sum, exercise) => sum + exercise.score, 0) || 0;
|
||||
const totalPoints =
|
||||
userExercisesStats?.reduce((sum, exercise) => sum + exercise.totalPoints, 0) || 0;
|
||||
const totalEarnedPoints = userExercisesStats?.reduce((sum, exercise) => sum + exercise.score, 0) || 0;
|
||||
const totalPoints = userExercisesStats?.reduce((sum, exercise) => sum + exercise.totalPoints, 0) || 0;
|
||||
const averageGrade = calcPercentageWithRounding(totalEarnedPoints, totalPoints);
|
||||
|
||||
return {
|
||||
@@ -224,10 +211,7 @@ async function getStudentOverview(courseId: string, member: any): Promise<Studen
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUserExercisesStats(
|
||||
courseId: string,
|
||||
userId: string
|
||||
): Promise<UserExercisesStats[] | undefined> {
|
||||
async function fetchUserExercisesStats(courseId: string, userId: string): Promise<UserExercisesStats[] | undefined> {
|
||||
try {
|
||||
const { data: courseData, error: queryError } = await supabase
|
||||
.from('course')
|
||||
|
||||
@@ -73,11 +73,7 @@ async function getAudienceData(userId: string, orgId: string): Promise<UserAnaly
|
||||
overallAverageGrade: 0
|
||||
};
|
||||
|
||||
const userResult = await supabase
|
||||
.from('profile')
|
||||
.select('fullname, email, avatar_url')
|
||||
.eq('id', userId)
|
||||
.single();
|
||||
const userResult = await supabase.from('profile').select('fullname, email, avatar_url').eq('id', userId).single();
|
||||
|
||||
if (userResult.error) throw new Error('Failed to fetch user profile');
|
||||
|
||||
@@ -112,21 +108,12 @@ async function getAudienceData(userId: string, orgId: string): Promise<UserAnaly
|
||||
});
|
||||
}
|
||||
|
||||
const totalLessons = audienceAnalytics.courses.reduce(
|
||||
(acc, course) => acc + course.lessons_count,
|
||||
0
|
||||
);
|
||||
const completedLessons = audienceAnalytics.courses.reduce(
|
||||
(acc, course) => acc + course.lessons_completed,
|
||||
0
|
||||
);
|
||||
const totalLessons = audienceAnalytics.courses.reduce((acc, course) => acc + course.lessons_count, 0);
|
||||
const completedLessons = audienceAnalytics.courses.reduce((acc, course) => acc + course.lessons_completed, 0);
|
||||
const overallCourseProgress = calcPercentageWithRounding(completedLessons, totalLessons);
|
||||
|
||||
const allGrades = sumArrObject(audienceAnalytics.courses, 'average_grade');
|
||||
const overallAverageGrade = calcPercentageWithRounding(
|
||||
allGrades,
|
||||
audienceAnalytics.courses.length
|
||||
);
|
||||
const overallAverageGrade = calcPercentageWithRounding(allGrades, audienceAnalytics.courses.length);
|
||||
|
||||
audienceAnalytics.overallCourseProgress = overallCourseProgress;
|
||||
audienceAnalytics.overallAverageGrade = overallAverageGrade || 0;
|
||||
@@ -134,10 +121,7 @@ async function getAudienceData(userId: string, orgId: string): Promise<UserAnaly
|
||||
return audienceAnalytics;
|
||||
}
|
||||
|
||||
async function getStudentAnalyticsData(
|
||||
userId: string,
|
||||
courseId: string
|
||||
): Promise<UserCourseAnalytics> {
|
||||
async function getStudentAnalyticsData(userId: string, courseId: string): Promise<UserCourseAnalytics> {
|
||||
const userCourseAnalytics: UserCourseAnalytics = {
|
||||
user: {
|
||||
id: userId,
|
||||
@@ -191,18 +175,12 @@ async function getStudentAnalyticsData(
|
||||
|
||||
console.log('completedLessons.length', completedLessons.length);
|
||||
console.log('lessons.length', lessons.length);
|
||||
userCourseAnalytics.progressPercentage = calcPercentageWithRounding(
|
||||
completedLessons.length,
|
||||
lessons.length
|
||||
);
|
||||
userCourseAnalytics.progressPercentage = calcPercentageWithRounding(completedLessons.length, lessons.length);
|
||||
|
||||
return userCourseAnalytics;
|
||||
}
|
||||
|
||||
async function fetchUserExercisesStats(
|
||||
courseId: string,
|
||||
userId: string
|
||||
): Promise<UserExercisesStats[] | undefined> {
|
||||
async function fetchUserExercisesStats(courseId: string, userId: string): Promise<UserExercisesStats[] | undefined> {
|
||||
try {
|
||||
const { data: courseData, error: queryError } = await supabase
|
||||
.from('course')
|
||||
|
||||
@@ -21,8 +21,7 @@ export async function POST({ request }) {
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'You are a teaching assistant, you only spit out JSON and nothing else, dont give any explanation.'
|
||||
content: 'You are a teaching assistant, you only spit out JSON and nothing else, dont give any explanation.'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
addDomainToVercel,
|
||||
getConfigResponse,
|
||||
removeDomainFromVercelProject
|
||||
} from '$lib/utils/services/org/domain';
|
||||
import { addDomainToVercel, getConfigResponse, removeDomainFromVercelProject } from '$lib/utils/services/org/domain';
|
||||
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
@@ -15,10 +11,7 @@ export async function POST({ request }) {
|
||||
}
|
||||
|
||||
if (params?.domain?.includes('classroomio.com')) {
|
||||
return json(
|
||||
{ success: false, message: 'Domain cannot contain classroomio.com' },
|
||||
{ status: 400 }
|
||||
);
|
||||
return json({ success: false, message: 'Domain cannot contain classroomio.com' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -5,12 +5,7 @@ import sendEmail from '$mail/sendEmail';
|
||||
|
||||
const supabase = getServerSupabase();
|
||||
|
||||
const sendEmailNotification = async (
|
||||
sFetch: typeof fetch,
|
||||
feedId: string,
|
||||
authorId: string,
|
||||
comment?: string
|
||||
) => {
|
||||
const sendEmailNotification = async (sFetch: typeof fetch, feedId: string, authorId: string, comment?: string) => {
|
||||
const feed = await getFeedForNotification({
|
||||
supabase,
|
||||
feedId,
|
||||
|
||||
@@ -7,14 +7,7 @@ import sendEmail from '$mail/sendEmail';
|
||||
export async function POST({ fetch, request }) {
|
||||
const { to, courseName, teacherEmail, studentFullname, orgName } = await request.json();
|
||||
// const accessToken = request.headers.get('Authorization') || '';
|
||||
console.log(
|
||||
'/POST api/email/course/student_prove_payment',
|
||||
to,
|
||||
courseName,
|
||||
teacherEmail,
|
||||
studentFullname,
|
||||
orgName
|
||||
);
|
||||
console.log('/POST api/email/course/student_prove_payment', to, courseName, teacherEmail, studentFullname, orgName);
|
||||
|
||||
if (!to || !courseName || !teacherEmail || !studentFullname || !orgName) {
|
||||
return json({ success: false, message: 'Missing required fields' }, { status: 400 });
|
||||
|
||||
@@ -3,13 +3,7 @@ import sendEmail from '$mail/sendEmail';
|
||||
|
||||
export async function POST({ fetch, request }) {
|
||||
const { to, courseName, studentEmail, studentFullname } = await request.json();
|
||||
console.log(
|
||||
'/POST api/email/course/teacher_student_buycourse',
|
||||
to,
|
||||
courseName,
|
||||
studentEmail,
|
||||
studentFullname
|
||||
);
|
||||
console.log('/POST api/email/course/teacher_student_buycourse', to, courseName, studentEmail, studentFullname);
|
||||
|
||||
if (!to || !courseName || !studentEmail || !studentFullname) {
|
||||
return json({ success: false, message: 'Missing required fields' }, { status: 400 });
|
||||
|
||||
@@ -9,10 +9,7 @@ export async function POST({ fetch, request }) {
|
||||
console.log('/POST api/email/invite', body);
|
||||
|
||||
if (!org || !Object.keys(org).length || !email) {
|
||||
return json(
|
||||
{ success: false, message: 'Org data and Teacher name are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
return json({ success: false, message: 'Org data and Teacher name are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { id, name, siteName } = org;
|
||||
|
||||
@@ -15,9 +15,7 @@ export async function POST({ fetch, request }) {
|
||||
orgSiteName
|
||||
});
|
||||
|
||||
const verificationLink = `${origin}/api/verify?data=${encodeURIComponent(
|
||||
btoa(verificationData)
|
||||
)}`;
|
||||
const verificationLink = `${origin}/api/verify?data=${encodeURIComponent(btoa(verificationData))}`;
|
||||
|
||||
const emailData = [
|
||||
{
|
||||
@@ -39,7 +37,6 @@ export async function POST({ fetch, request }) {
|
||||
await sendEmail(fetch)(emailData);
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
message: `Email sent ${verificationLink}`
|
||||
success: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
|
||||
import { cancelOrgPlan, createOrgPlan, updateOrgPlan } from '$lib/utils/services/org';
|
||||
|
||||
import { PLAN } from '@cio/utils/plans';
|
||||
import type { PolarWebhookPayload } from '$lib/utils/types/polar';
|
||||
import { Webhooks } from '@polar-sh/sveltekit';
|
||||
import { PLAN } from 'shared/src/plans/constants';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
|
||||
|
||||
export const POST = Webhooks({
|
||||
webhookSecret: env.POLAR_WEBHOOK_SECRET!,
|
||||
|
||||
@@ -28,9 +28,6 @@ export async function POST({ request }) {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
return json(
|
||||
{ success: false, message: 'Error fetching photos from Unsplash' },
|
||||
{ status: 500 }
|
||||
);
|
||||
return json({ success: false, message: 'Error fetching photos from Unsplash' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -22,8 +22,8 @@ export const GET = async ({ url }) => {
|
||||
|
||||
if (error) {
|
||||
console.log('profile not found', error);
|
||||
throw redirect(307, '/404');
|
||||
redirect(307, '/404');
|
||||
}
|
||||
|
||||
throw redirect(307, `/org/${orgSiteName}?welcomePopup=true`);
|
||||
redirect(307, `/org/${orgSiteName}?welcomePopup=true`);
|
||||
};
|
||||
@@ -6,17 +6,11 @@
|
||||
course: Course;
|
||||
}
|
||||
|
||||
export let data: Data;
|
||||
|
||||
let courseData: Course;
|
||||
|
||||
function setCourseData(course: Course | null) {
|
||||
if (!course) return;
|
||||
|
||||
courseData = { ...course };
|
||||
interface Props {
|
||||
data: Data;
|
||||
}
|
||||
|
||||
$: setCourseData(data.course);
|
||||
let { data }: Props = $props();
|
||||
</script>
|
||||
|
||||
<CourseLandingPage {courseData} />
|
||||
<CourseLandingPage courseData={data.course} />
|
||||
|
||||
@@ -1,34 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { sideBar } from '$lib/components/Org/store';
|
||||
import { apps } from '$lib/components/Apps/store';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import APPS_CONSTANTS from '$lib/components/Apps/constants';
|
||||
import hotkeys from 'hotkeys-js';
|
||||
|
||||
function handleClose() {
|
||||
$apps.selectedApp = undefined;
|
||||
if ($globalStore.isStudent) {
|
||||
$apps.open = false;
|
||||
}
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
function handleAppClick(appName?: string) {
|
||||
if (appName === $apps.selectedApp) {
|
||||
handleClose();
|
||||
} else {
|
||||
$apps.selectedApp = appName;
|
||||
}
|
||||
$apps.open = true;
|
||||
$apps.dropdown = !$apps.dropdown;
|
||||
}
|
||||
let { children }: Props = $props();
|
||||
|
||||
function toggleSidebar() {
|
||||
$sideBar.hidden = !$sideBar.hidden;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
hotkeys('ctrl+b,command+b,ctrl+1,command+1,ctrl+2,command+2', function (event, handler) {
|
||||
hotkeys('ctrl+b,command+b', function (event, handler) {
|
||||
// Prevent default behavior
|
||||
event.preventDefault();
|
||||
|
||||
@@ -39,17 +24,9 @@
|
||||
case 'command+b':
|
||||
toggleSidebar();
|
||||
break;
|
||||
case 'ctrl+1':
|
||||
case 'command+1':
|
||||
handleAppClick(APPS_CONSTANTS.APPS.COMMENTS);
|
||||
break;
|
||||
case 'ctrl+2':
|
||||
case 'command+2':
|
||||
handleAppClick(APPS_CONSTANTS.APPS.POLL);
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import Box from '$lib/components/Box/index.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 { CourseContainer } from '$lib/components/CourseContainer';
|
||||
import { PageBody, PageNav } from '$lib/components/Page';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
@@ -23,32 +23,35 @@
|
||||
handleEditFeed,
|
||||
toggleFeedIsPinned
|
||||
} from '$lib/utils/services/newsfeed';
|
||||
import {
|
||||
NOTIFICATION_NAME,
|
||||
triggerSendEmail
|
||||
} from '$lib/utils/services/notification/notification';
|
||||
import { NOTIFICATION_NAME, triggerSendEmail } from '$lib/utils/services/notification/notification';
|
||||
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 PinFilled from 'carbon-icons-svelte/lib/PinFilled.svelte';
|
||||
import PinIcon from '@lucide/svelte/icons/pin';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let data;
|
||||
let { data = $bindable() } = $props();
|
||||
|
||||
let createdComment;
|
||||
let edit = false;
|
||||
let isLoading = false;
|
||||
let editFeed: Feed;
|
||||
let author = {
|
||||
id: '',
|
||||
username: '',
|
||||
fullname: '',
|
||||
avatar_url: ''
|
||||
};
|
||||
let edit = $state(false);
|
||||
let isLoading = $state(false);
|
||||
let editFeed: Feed | null = $state(null);
|
||||
|
||||
let query = new URLSearchParams($page.url.search);
|
||||
let query = new URLSearchParams(page.url.search);
|
||||
let feedId = query.get('feedId') || '';
|
||||
|
||||
const author = $derived.by(() => {
|
||||
const groupMember = $group.people.find((person) => person.profile_id === $profile.id);
|
||||
|
||||
return {
|
||||
id: groupMember?.id || '',
|
||||
username: $profile.username || '',
|
||||
fullname: $profile.fullname || '',
|
||||
avatar_url: $profile.avatar_url || ''
|
||||
};
|
||||
});
|
||||
|
||||
const onEdit = (id: string, content: string) => {
|
||||
handleEditFeed(id, content);
|
||||
|
||||
@@ -81,9 +84,7 @@
|
||||
let reactedAuthorIds: string[] = reactedFeed.reaction[reactionType];
|
||||
|
||||
if (reactedAuthorIds.includes(authorId)) {
|
||||
reactedAuthorIds = reactedAuthorIds.filter(
|
||||
(reactionAuthorId) => reactionAuthorId !== authorId
|
||||
);
|
||||
reactedAuthorIds = reactedAuthorIds.filter((reactionAuthorId) => reactionAuthorId !== authorId);
|
||||
} else {
|
||||
reactedAuthorIds = [...reactedAuthorIds, authorId];
|
||||
}
|
||||
@@ -199,10 +200,12 @@
|
||||
snackbar.error('snackbar.course.error.news_feed_fail');
|
||||
}
|
||||
if (data) {
|
||||
$newsFeed = data.map((feedItem) => ({
|
||||
...feedItem,
|
||||
isPinned: feedItem.is_pinned
|
||||
}));
|
||||
$newsFeed = data
|
||||
.map((feedItem) => ({
|
||||
...feedItem,
|
||||
isPinned: feedItem.is_pinned
|
||||
}))
|
||||
.sort((a, b) => Number(b.isPinned) - Number(a.isPinned));
|
||||
}
|
||||
} catch (error) {
|
||||
snackbar.error('snackbar.course.error.feed_fetch_error');
|
||||
@@ -211,17 +214,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
function setAuthor(groups, profileId) {
|
||||
const currentGroupMember = groups.people.find((person) => person.profile_id === profileId);
|
||||
|
||||
author = {
|
||||
id: currentGroupMember?.id || '',
|
||||
username: $profile.username || '',
|
||||
fullname: $profile.fullname || '',
|
||||
avatar_url: $profile.avatar_url || ''
|
||||
};
|
||||
}
|
||||
|
||||
function getPageRoles(org: CurrentOrg) {
|
||||
const roles: number[] = [1, 2];
|
||||
|
||||
@@ -232,13 +224,12 @@
|
||||
return roles;
|
||||
}
|
||||
|
||||
$: initNewsFeed(data.courseId);
|
||||
|
||||
$: setAuthor($group, $profile.id);
|
||||
$: $newsFeed = $newsFeed.sort((a, b) => Number(b.isPinned) - Number(a.isPinned));
|
||||
onMount(() => {
|
||||
initNewsFeed(data.courseId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<CourseContainer bind:courseId={data.courseId}>
|
||||
<CourseContainer courseId={data.courseId}>
|
||||
<RoleBasedSecurity
|
||||
allowedRoles={getPageRoles($currentOrg)}
|
||||
onDenied={() => {
|
||||
@@ -246,7 +237,7 @@
|
||||
}}
|
||||
>
|
||||
<PageNav title={$t('course.navItem.news_feed.heading')} disableSticky={true}>
|
||||
<slot:fragment slot="widget">
|
||||
{#snippet widget()}
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<PrimaryButton
|
||||
className="mr-2"
|
||||
@@ -254,7 +245,7 @@
|
||||
onClick={() => ($isNewFeedModal.open = true)}
|
||||
/>
|
||||
</RoleBasedSecurity>
|
||||
</slot:fragment>
|
||||
{/snippet}
|
||||
</PageNav>
|
||||
|
||||
<PageBody width="max-w-4xl px-3">
|
||||
@@ -290,7 +281,7 @@
|
||||
{#each $newsFeed as feed}
|
||||
{#if feed.isPinned}
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<PinFilled size={16} />
|
||||
<PinIcon size={16} class="filled" />
|
||||
|
||||
<p class="text-sm">{$t('course.navItem.news_feed.pinned')}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
|
||||
import ChartBarIcon from '@lucide/svelte/icons/chart-bar';
|
||||
import { CourseContainer } from '$lib/components/CourseContainer';
|
||||
import { PageBody, PageNav } from '$lib/components/Page';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { getAccessToken } from '$lib/utils/functions/supabase';
|
||||
@@ -11,17 +12,16 @@
|
||||
import AnalyticsGraph from '$lib/components/Course/components/Analytics/AnalyticsGraph.svelte';
|
||||
import AnalyticsSkeleton from '$lib/components/Course/components/Analytics/AnalyticsSkeleton.svelte';
|
||||
import AnalyticsCard from '$lib/components/Course/components/Analytics/AnalyticsCard.svelte';
|
||||
import { ChartBar } from 'carbon-icons-svelte';
|
||||
import StudentsIcon from '$lib/components/Course/components/Analytics/icons/StudentsIcon.svelte';
|
||||
import LessonsIcon from '$lib/components/Course/components/Analytics/icons/LessonsIcon.svelte';
|
||||
import ProgressIcon from '$lib/components/Course/components/Analytics/icons/ProgressIcon.svelte';
|
||||
import ExercisesIcon from '$lib/components/Course/components/Analytics/icons/ExercisesIcon.svelte';
|
||||
import EmptyState from '$lib/components/Course/components/Analytics/EmptyState.svelte';
|
||||
|
||||
export let data;
|
||||
let { data = $bindable() } = $props();
|
||||
|
||||
let courseAnalytics: CourseAnalytics | null = null;
|
||||
let isLoading = true;
|
||||
let courseAnalytics: CourseAnalytics | null = $state(null);
|
||||
let isLoading = $state(true);
|
||||
|
||||
async function fetchCourseAnalytics() {
|
||||
try {
|
||||
@@ -58,7 +58,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<CourseContainer bind:courseId={data.courseId}>
|
||||
<CourseContainer courseId={data.courseId}>
|
||||
<PageNav title={$t('course.navItem.analytics.title')} />
|
||||
<PageBody width="w-full max-w-6xl md:w-11/12">
|
||||
{#if isLoading}
|
||||
@@ -114,9 +114,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Students Overview Table -->
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-neutral-800"
|
||||
>
|
||||
<div class="rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-neutral-800">
|
||||
<div class="border-b border-gray-200 px-6 py-4 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{$t('analytics.student_progress_overview')}
|
||||
@@ -132,7 +130,7 @@
|
||||
<EmptyState
|
||||
title={$t('analytics.no_analytics_data')}
|
||||
description={$t('analytics.no_analytics_description')}
|
||||
icon={ChartBar}
|
||||
icon={ChartBarIcon}
|
||||
className="h-screen"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<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 { CourseContainer } from '$lib/components/CourseContainer';
|
||||
import { PageBody, PageNav } from '$lib/components/Page';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { ROLE } from '$lib/utils/constants/roles';
|
||||
@@ -24,9 +23,9 @@
|
||||
StructuredListHead,
|
||||
StructuredListRow
|
||||
} from 'carbon-components-svelte';
|
||||
import AudioConsoleIcon from 'carbon-icons-svelte/lib/AudioConsole.svelte';
|
||||
import BadgeXIcon from '@lucide/svelte/icons/badge-x';
|
||||
|
||||
export let data;
|
||||
let { data = $bindable() } = $props();
|
||||
|
||||
interface CourseData {
|
||||
attendance: {
|
||||
@@ -37,8 +36,14 @@
|
||||
}[];
|
||||
}
|
||||
|
||||
let students: GroupPerson[] = [];
|
||||
let searchValue = '';
|
||||
let hasFetched = $state(false);
|
||||
|
||||
const students: GroupPerson[] = $derived(
|
||||
$globalStore.isStudent
|
||||
? $group.people.filter((person) => !!person.profile && person.profile.id === $profile.id)
|
||||
: $group.people.filter((person) => !!person.profile && person.role_id === ROLE.STUDENT)
|
||||
);
|
||||
let searchValue = $state('');
|
||||
|
||||
function setAttendance(courseData: CourseData) {
|
||||
for (const attendanceItem of courseData.attendance) {
|
||||
@@ -115,34 +120,30 @@
|
||||
// function for the searchbar
|
||||
function searchStudents(query: string, _students: GroupPerson[]) {
|
||||
const lowercaseQuery = query.toLowerCase();
|
||||
return _students.filter((student) =>
|
||||
student.profile?.fullname?.toLowerCase()?.includes(lowercaseQuery)
|
||||
);
|
||||
return _students.filter((student) => student.profile?.fullname?.toLowerCase()?.includes(lowercaseQuery));
|
||||
}
|
||||
|
||||
async function firstRender(courseId: string) {
|
||||
if (courseId) {
|
||||
if (!Object.keys($attendance).length) {
|
||||
setAttendance($course);
|
||||
}
|
||||
return;
|
||||
async function firstRender(courseId?: string) {
|
||||
if (!courseId || hasFetched) return;
|
||||
|
||||
hasFetched = true;
|
||||
|
||||
if (!Object.keys($attendance).length) {
|
||||
setAttendance($course);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$: students = $globalStore.isStudent
|
||||
? $group.people.filter((person) => !!person.profile && person.profile.id === $profile.id)
|
||||
: $group.people.filter((person) => !!person.profile && person.role_id === ROLE.STUDENT);
|
||||
|
||||
$: browser && $course.id && firstRender($course.id);
|
||||
$effect(() => {
|
||||
firstRender($course.id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<CourseContainer bind:courseId={data.courseId}>
|
||||
<CourseContainer courseId={data.courseId}>
|
||||
<PageNav title={$t('course.navItem.attendance.title')} />
|
||||
<PageBody width="w-full max-w-6xl md:w-11/12">
|
||||
<section class="mx-2 my-5 flex items-center lg:mx-9">
|
||||
<div
|
||||
class="flex w-full flex-col items-start justify-between gap-2 lg:flex-row lg:items-center"
|
||||
>
|
||||
<div class="flex w-full flex-col items-start justify-between gap-2 lg:flex-row lg:items-center">
|
||||
<div class="flex">
|
||||
<p class="mr-5 flex items-center">
|
||||
<Checkbox checked disabled />
|
||||
@@ -171,11 +172,9 @@
|
||||
<StructuredListCell head class="text-primary-600 py-3"
|
||||
>{$t('course.navItem.attendance.student')}</StructuredListCell
|
||||
>
|
||||
{#each $lessons as lesson, index}
|
||||
{#each $lessons as _lesson, index}
|
||||
<StructuredListCell head class="text-primary-600 py-3"
|
||||
>{$t('course.navItem.attendance.lesson')} 0{getLectureNo(
|
||||
index + 1
|
||||
)}</StructuredListCell
|
||||
>{$t('course.navItem.attendance.lesson')} 0{getLectureNo(index + 1)}</StructuredListCell
|
||||
>
|
||||
{/each}
|
||||
</StructuredListRow>
|
||||
@@ -211,7 +210,7 @@
|
||||
</StructuredList>
|
||||
{#if students.length === 0}
|
||||
<Box className="h-[300px] w-full">
|
||||
<AudioConsoleIcon size={32} class="carbon-icon w-80" />
|
||||
<BadgeXIcon size={48} />
|
||||
<h3 class="text-center text-3xl text-gray-500 dark:text-white">
|
||||
{$t('course.navItem.attendance.no_student')}
|
||||
</h3>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import IssueCertificateModal from '$lib/components/Course/components/Ceritficate/IssueCertificateModal.svelte';
|
||||
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
|
||||
import { CourseContainer } from '$lib/components/CourseContainer';
|
||||
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';
|
||||
@@ -12,23 +12,25 @@
|
||||
import StudentCertificate from '$lib/components/Course/components/Ceritficate/StudentCertificate/Index.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
export let data;
|
||||
let { data = $bindable() } = $props();
|
||||
</script>
|
||||
|
||||
<IssueCertificateModal />
|
||||
|
||||
<CourseContainer bind:courseId={data.courseId}>
|
||||
<CourseContainer courseId={data.courseId}>
|
||||
<PageNav title={$t('course.navItem.certificates.title')} disableSticky={true}>
|
||||
<slot:fragment slot="widget">
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<!-- Hide Functionality For V1 -->
|
||||
<!-- <PrimaryButton
|
||||
className="mr-2 rounded-md text-sm font-medium"
|
||||
label="Issue Cert"
|
||||
onClick={() => ($issueCertificateModal.open = true)}
|
||||
/> -->
|
||||
</RoleBasedSecurity>
|
||||
</slot:fragment>
|
||||
{#snippet widget()}
|
||||
<slot:fragment>
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<!-- Hide Functionality For V1 -->
|
||||
<!-- <PrimaryButton
|
||||
className="mr-2 rounded-md text-sm font-medium"
|
||||
label="Issue Cert"
|
||||
onClick={() => ($issueCertificateModal.open = true)}
|
||||
/> -->
|
||||
</RoleBasedSecurity>
|
||||
</slot:fragment>
|
||||
{/snippet}
|
||||
</PageNav>
|
||||
|
||||
<PageBody className="w-full mx-0" padding="px-3 pb-10">
|
||||
|
||||
@@ -1,41 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { dev } from '$app/environment';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import type { Course, Lesson, LessonSection } from '$lib/utils/types';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { lessons, lessonSections } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import { CourseContainer } from '$lib/components/CourseContainer';
|
||||
|
||||
import CourseLandingPage from '$lib/components/CourseLandingPage/index.svelte';
|
||||
import Editor from '$lib/components/CourseLandingPage/components/Editor/index.svelte';
|
||||
|
||||
export let data;
|
||||
let { data } = $props();
|
||||
|
||||
const { courseId } = data;
|
||||
|
||||
let courseData: Course = $course;
|
||||
|
||||
function setCourseData(course: Course, lessons: Lesson[], lesson_section: LessonSection[]) {
|
||||
courseData = { ...course, lessons, lesson_section };
|
||||
}
|
||||
let courseData: Course = $derived({
|
||||
...$course,
|
||||
$lessons,
|
||||
$lessonSections
|
||||
});
|
||||
|
||||
function syncCourseStore(_courseData: Course) {
|
||||
$course = _courseData;
|
||||
}
|
||||
|
||||
$: setCourseData($course, $lessons, $lessonSections);
|
||||
$: dev && console.log('courseData changed', courseData);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="absolute flex inset-0 z-50 bg-white"
|
||||
in:fly={{ y: 500, duration: 500 }}
|
||||
out:fly={{ y: 500, duration: 500 }}
|
||||
>
|
||||
<Editor {courseId} bind:course={courseData} {syncCourseStore} />
|
||||
<div class="rightBar">
|
||||
<CourseLandingPage bind:courseData editMode={true} />
|
||||
<CourseContainer {courseId} renderOnlyChildren={true}>
|
||||
<div
|
||||
class="absolute inset-0 z-50 flex bg-white"
|
||||
in:fly={{ y: 500, duration: 500 }}
|
||||
out:fly={{ y: 500, duration: 500 }}
|
||||
>
|
||||
<Editor {courseId} bind:course={courseData} {syncCourseStore} />
|
||||
<div class="rightBar">
|
||||
<CourseLandingPage bind:courseData editMode={true} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CourseContainer>
|
||||
|
||||
<style>
|
||||
.rightBar {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
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';
|
||||
@@ -8,13 +8,9 @@
|
||||
import LessonSectionList from '$lib/components/Course/components/Lesson/LessonSectionList.svelte';
|
||||
import NewLessonModal from '$lib/components/Course/components/Lesson/NewLessonModal.svelte';
|
||||
import { handleAddLessonWidget } from '$lib/components/Course/components/Lesson/store';
|
||||
import {
|
||||
handleDelete,
|
||||
lessons,
|
||||
lessonSections
|
||||
} from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
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 { CourseContainer } from '$lib/components/CourseContainer';
|
||||
import { PageBody, PageNav } from '$lib/components/Page';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
@@ -24,16 +20,18 @@
|
||||
import type { Lesson } from '$lib/utils/types';
|
||||
import { COURSE_VERSION } from '$lib/utils/types';
|
||||
|
||||
export let data;
|
||||
let { data } = $props();
|
||||
|
||||
const query = new URLSearchParams($page.url.search);
|
||||
const query = new URLSearchParams(page.url.search);
|
||||
|
||||
const lessonsLength = $derived($course.version === COURSE_VERSION.V1 ? $lessons.length : $lessonSections.length);
|
||||
|
||||
let lessonEditing: string | undefined;
|
||||
let lessonToDelete: Lesson | undefined;
|
||||
let openDeleteModal: boolean = false;
|
||||
let isFetching: boolean = false;
|
||||
let reorder = false;
|
||||
let activateSections = false;
|
||||
let lessonToDelete: Lesson | undefined = $state();
|
||||
let openDeleteModal: boolean = $state(false);
|
||||
let isFetching: boolean = $state(false);
|
||||
let reorder = $state(false);
|
||||
let activateSections = $state(false);
|
||||
|
||||
function addLesson() {
|
||||
$handleAddLessonWidget.open = true;
|
||||
@@ -77,48 +75,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: shouldGoToNextLesson = query.get('next') === 'true';
|
||||
$: !isFetching && shouldGoToNextLesson && onNextQuery($lessons);
|
||||
$: lessonsLength =
|
||||
$course.version === COURSE_VERSION.V1 ? $lessons.length : $lessonSections.length;
|
||||
let shouldGoToNextLesson = $derived(query.get('next') === 'true');
|
||||
$effect(() => {
|
||||
!isFetching && shouldGoToNextLesson && onNextQuery($lessons);
|
||||
});
|
||||
</script>
|
||||
|
||||
<NewLessonModal />
|
||||
|
||||
<ActivateSectionsModal bind:open={activateSections} />
|
||||
|
||||
<DeleteLessonConfirmation
|
||||
bind:openDeleteModal
|
||||
deleteLesson={() => handleDelete(lessonToDelete?.id)}
|
||||
/>
|
||||
<DeleteLessonConfirmation bind:openDeleteModal deleteLesson={() => handleDelete(lessonToDelete?.id)} />
|
||||
|
||||
<CourseContainer bind:isFetching bind:courseId={data.courseId}>
|
||||
<CourseContainer
|
||||
onFetchingChange={(fetching) => {
|
||||
isFetching = fetching;
|
||||
}}
|
||||
courseId={data.courseId}
|
||||
>
|
||||
<PageNav title={$t('course.navItem.lessons.heading_v2')}>
|
||||
<div slot="widget" class="flex w-full justify-end gap-2">
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
{#if $course.version === COURSE_VERSION.V1}
|
||||
{#snippet widget()}
|
||||
<div class="flex w-full justify-end gap-2">
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
{#if $course.version === COURSE_VERSION.V1}
|
||||
<PrimaryButton
|
||||
label={$t(`course.navItem.lessons.section_prompt.cta`)}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => (activateSections = !activateSections)}
|
||||
isDisabled={!!lessonEditing}
|
||||
/>
|
||||
{/if}
|
||||
<PrimaryButton
|
||||
label={$t(`course.navItem.lessons.section_prompt.cta`)}
|
||||
label={$t(`course.navItem.lessons.add_lesson.${reorder ? 'end_reorder' : 'start_reorder'}`)}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => (activateSections = !activateSections)}
|
||||
onClick={() => (reorder = !reorder)}
|
||||
isDisabled={!!lessonEditing}
|
||||
/>
|
||||
{/if}
|
||||
<PrimaryButton
|
||||
label={$t(
|
||||
`course.navItem.lessons.add_lesson.${reorder ? 'end_reorder' : 'start_reorder'}`
|
||||
)}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => (reorder = !reorder)}
|
||||
isDisabled={!!lessonEditing}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.add_lesson.button_title')}
|
||||
onClick={addLesson}
|
||||
isDisabled={!!lessonEditing}
|
||||
/>
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.add_lesson.button_title')}
|
||||
onClick={addLesson}
|
||||
isDisabled={!!lessonEditing}
|
||||
/>
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
{/snippet}
|
||||
</PageNav>
|
||||
|
||||
<PageBody width="max-w-6xl" padding="p-0">
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export const load = ({ params = { id: '' } }) => {
|
||||
export const load = ({ params }) => {
|
||||
const { id: courseId, lessonParams = '' } = params;
|
||||
const splitparams = lessonParams.split('/');
|
||||
|
||||
@@ -1,95 +1,83 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
|
||||
import {
|
||||
checkExercisesComplete,
|
||||
fetchLesson,
|
||||
updateLessonCompletion
|
||||
} from '$lib/utils/services/courses';
|
||||
import { page } from '$app/state';
|
||||
import { CourseContainer } from '$lib/components/CourseContainer';
|
||||
import { checkExercisesComplete, fetchLesson, updateLessonCompletion } from '$lib/utils/services/courses';
|
||||
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 ListChecksIcon from '@lucide/svelte/icons/list-checks';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
|
||||
import PencilIcon from '@lucide/svelte/icons/pencil';
|
||||
import SaveIcon from '@lucide/svelte/icons/save';
|
||||
import ChevronLeftIcon from '@lucide/svelte/icons/chevron-left';
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
import HistoryIcon from '@lucide/svelte/icons/history';
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { apps } from '$lib/components/Apps/store';
|
||||
import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte';
|
||||
import { getIsLessonComplete } from '$lib/components/Course/components/Lesson/functions';
|
||||
import LessonVersionHistory from '$lib/components/Course/components/Lesson/LessonVersionHistory.svelte';
|
||||
import Materials from '$lib/components/Course/components/Lesson/Materials/index.svelte';
|
||||
import {
|
||||
lesson,
|
||||
lessonByTranslation,
|
||||
lessons,
|
||||
lessonSections
|
||||
} from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import { lesson, setLesson, lessons, lessonSections } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import { getGroupMemberId } from '$lib/components/Course/function';
|
||||
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 { IconButton } from '$lib/components/IconButton';
|
||||
import { PageBody, PageNav } from '$lib/components/Page';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import MODES from '$lib/utils/constants/mode';
|
||||
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 OverflowMenuVertical from 'carbon-icons-svelte/lib/OverflowMenuVertical.svelte';
|
||||
import ResultOld from 'carbon-icons-svelte/lib/ResultOld.svelte';
|
||||
import { classroomio } from '$lib/utils/services/api';
|
||||
|
||||
export let data;
|
||||
let { data = $bindable() } = $props();
|
||||
|
||||
let path = '';
|
||||
let mode = MODES.view;
|
||||
let prevMode = '';
|
||||
let isMarkingComplete = false;
|
||||
let isLoading = false;
|
||||
let isSaving = false;
|
||||
let isLessonComplete = false;
|
||||
let isVersionDrawerOpen = false;
|
||||
let mode = $state(MODES.view);
|
||||
let prevMode = $state('');
|
||||
let isMarkingComplete = $state(false);
|
||||
let isSaving = $state(false);
|
||||
let isVersionDrawerOpen = $state(false);
|
||||
let hasFetched = $state('');
|
||||
|
||||
function getLessonOrder(id: string) {
|
||||
const index = $lessons.findIndex((lesson) => lesson.id === id);
|
||||
|
||||
if (index < 10) {
|
||||
return '0' + (index + 1);
|
||||
} else {
|
||||
return index + 1;
|
||||
}
|
||||
}
|
||||
const path = $derived(page.url?.pathname?.replace(/\/exercises[\/ 0-9 a-z -]*/, ''));
|
||||
const isLessonComplete = $derived(getIsLessonComplete($lesson.lesson_completion, $profile.id));
|
||||
|
||||
async function fetchReqData(lessonId = '', isMaterialsTabActive: boolean) {
|
||||
const timeout = setTimeout(() => {
|
||||
$lesson.isFetching = true;
|
||||
}, 500);
|
||||
if (lessonId === $lesson.id) return;
|
||||
|
||||
if (!lessonId || hasFetched === lessonId) return;
|
||||
|
||||
hasFetched = lessonId;
|
||||
|
||||
$lesson.isFetching = true;
|
||||
console.log('fetching', $lesson.isFetching);
|
||||
|
||||
let lessonData;
|
||||
if (isMaterialsTabActive) {
|
||||
const lesson = await fetchLesson(lessonId);
|
||||
lessonData = lesson.data;
|
||||
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
console.log({ lessonData });
|
||||
|
||||
const totalExercises = lessonData?.totalExercises?.[0]?.count || 0;
|
||||
const totalComments = lessonData?.totalComments?.[0]?.count || 0;
|
||||
setLesson(lessonData, totalExercises, totalComments);
|
||||
|
||||
setLesson({
|
||||
id: lessonId,
|
||||
lessonData,
|
||||
totalExercises,
|
||||
totalComments,
|
||||
locale: $profile.locale
|
||||
});
|
||||
$lesson.isFetching = false;
|
||||
}
|
||||
|
||||
async function markLessonComplete(lessonId: string) {
|
||||
const groupMemberId = getGroupMemberId($group.people, $profile.id);
|
||||
const { data: areExercisesComplete, error } = await checkExercisesComplete(
|
||||
lessonId,
|
||||
groupMemberId
|
||||
);
|
||||
const { data: areExercisesComplete, error } = await checkExercisesComplete(lessonId, groupMemberId);
|
||||
|
||||
if (error) {
|
||||
snackbar.error('snackbar.lessons.error.try_later');
|
||||
@@ -147,115 +135,11 @@
|
||||
isMarkingComplete = false;
|
||||
}
|
||||
|
||||
const downloadLesson = async () => {
|
||||
const currentLesson = $lessons.find((l) => l.id === $lesson?.id);
|
||||
if (!currentLesson) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
const lessonVideo = $lesson.materials.videos.map((video) => video.link);
|
||||
const lessonNumber = getLessonOrder(currentLesson.id);
|
||||
const slideUrl = $lesson.materials.slide_url || '';
|
||||
|
||||
const response = await classroomio.course.lesson.download.pdf.$post({
|
||||
json: {
|
||||
title: currentLesson.title,
|
||||
number: lessonNumber,
|
||||
orgName: $currentOrg.name,
|
||||
note: $lesson.materials.note,
|
||||
slideUrl: slideUrl,
|
||||
video: lessonVideo,
|
||||
courseTitle: $course.title
|
||||
}
|
||||
});
|
||||
const blob = await response.blob();
|
||||
|
||||
const file = new Blob([blob], { type: 'application/pdf' });
|
||||
const fileURL = URL.createObjectURL(file);
|
||||
|
||||
let a = document.createElement('a');
|
||||
document.body.append(a);
|
||||
a.download = $course.title + ' - ' + 'Lesson ' + lessonNumber;
|
||||
a.href = fileURL;
|
||||
a.click();
|
||||
a.remove();
|
||||
|
||||
snackbar.success('snackbar.lessons.success.complete_download');
|
||||
} catch (error) {
|
||||
console.log('error downloading lesson', error);
|
||||
snackbar.error('snackbar.lessons.error.try_later');
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
};
|
||||
|
||||
function setLesson(lessonData, totalExercises: number, totalComments: number) {
|
||||
if (!lessonData) return;
|
||||
|
||||
let lesson_completion: LessonCompletion[] = [];
|
||||
|
||||
if (Array.isArray(lessonData.lesson_completion)) {
|
||||
lesson_completion = [...lessonData.lesson_completion];
|
||||
}
|
||||
|
||||
lesson.update((l) => ({
|
||||
...l,
|
||||
id: data.lessonId,
|
||||
title: lessonData.title,
|
||||
totalExercises,
|
||||
totalComments: totalComments,
|
||||
materials: {
|
||||
videos: lessonData.videos,
|
||||
note: lessonData.note,
|
||||
slide_url: lessonData.slide_url,
|
||||
documents: lessonData.documents || null
|
||||
},
|
||||
lesson_completion,
|
||||
exercises: [],
|
||||
locale: $profile.locale
|
||||
}));
|
||||
|
||||
if (Array.isArray(lessonData.lesson_language)) {
|
||||
lessonByTranslation.update((lessLocales) => {
|
||||
return {
|
||||
...lessLocales,
|
||||
[data.lessonId]: lessonData.lesson_language.reduce(
|
||||
(acc, cur) => {
|
||||
acc[cur.locale] = cur.content;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
en: ''
|
||||
}
|
||||
)
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const toggleApps = () => {
|
||||
$apps.open = !$apps.open;
|
||||
};
|
||||
|
||||
function toggleMode() {
|
||||
prevMode = mode;
|
||||
mode = mode === MODES.edit ? MODES.view : MODES.edit;
|
||||
}
|
||||
|
||||
function handleAppClick(appName: string) {
|
||||
if ($apps.selectedApp === appName) {
|
||||
$apps.selectedApp = undefined;
|
||||
$apps.open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$apps.selectedApp = appName;
|
||||
$apps.open = true;
|
||||
}
|
||||
|
||||
const getLessons = () => {
|
||||
if ($course.version === COURSE_VERSION.V1) {
|
||||
return $lessons;
|
||||
@@ -299,31 +183,31 @@
|
||||
isVersionDrawerOpen = false;
|
||||
if (data.courseId && browser) {
|
||||
mode = MODES.view;
|
||||
hasFetched = '';
|
||||
fetchReqData(data.lessonId, data.isMaterialsTabActive);
|
||||
}
|
||||
|
||||
snackbar.success('snackbar.lessons.success.version_restored');
|
||||
};
|
||||
|
||||
$: path = $page.url?.pathname?.replace(/\/exercises[\/ 0-9 a-z -]*/, '');
|
||||
$effect(() => {
|
||||
if (data.courseId && $profile.id) {
|
||||
fetchReqData(data.lessonId, data.isMaterialsTabActive);
|
||||
}
|
||||
});
|
||||
|
||||
$: if (data.courseId && $profile.id && browser) {
|
||||
mode = MODES.view;
|
||||
fetchReqData(data.lessonId, data.isMaterialsTabActive);
|
||||
}
|
||||
|
||||
$: isLessonComplete = getIsLessonComplete($lesson.lesson_completion, $profile.id);
|
||||
$: isPrevDisabled = isNextOrPrevDisabled(data.lessonId, true);
|
||||
$: isNextDisabled = isNextOrPrevDisabled(data.lessonId, false);
|
||||
const isPrevDisabled = $derived(isNextOrPrevDisabled(data.lessonId, true));
|
||||
const isNextDisabled = $derived(isNextOrPrevDisabled(data.lessonId, false));
|
||||
</script>
|
||||
|
||||
<CourseContainer
|
||||
{path}
|
||||
isExercisePage={!data.isMaterialsTabActive && !!data.exerciseId}
|
||||
bind:courseId={data.courseId}
|
||||
containerClass="relative"
|
||||
courseId={data.courseId}
|
||||
isExercisePage={!data.isMaterialsTabActive && !!data.exerciseId}
|
||||
>
|
||||
<PageNav
|
||||
bind:hideOnMobile={$globalStore.isStudent}
|
||||
hideOnMobile={$globalStore.isStudent}
|
||||
navItems={[
|
||||
{
|
||||
label: $t('course.navItem.lessons.lesson_nav.materials'),
|
||||
@@ -338,40 +222,28 @@
|
||||
}
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="widget">
|
||||
{#snippet widget()}
|
||||
<div class="flex items-center gap-1">
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
{#if data.isMaterialsTabActive}
|
||||
<!-- Version control -->
|
||||
{#if mode === MODES.edit && window.innerWidth >= 1024}
|
||||
<IconButton onClick={() => (isVersionDrawerOpen = true)}>
|
||||
<ResultOld size={24} />
|
||||
<HistoryIcon size={20} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
|
||||
<div class="tab">
|
||||
<IconButton onClick={toggleApps} buttonClassName="">
|
||||
<OverflowMenuVertical size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div
|
||||
class={`flex-row ${
|
||||
$apps.dropdown && $apps.open
|
||||
? 'absolute right-14 top-[85px] z-40 rounded-md bg-gray-100 p-3 lg:relative lg:right-0 lg:top-0 lg:p-0 dark:bg-neutral-800'
|
||||
: 'hidden'
|
||||
} items-center lg:flex`}
|
||||
>
|
||||
<div class="hidden flex-row items-center lg:flex">
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
$apps.dropdown = false;
|
||||
toggleMode();
|
||||
}}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{#if mode === MODES.edit}
|
||||
<Save size={24} />
|
||||
<SaveIcon size={20} />
|
||||
{:else}
|
||||
<Edit size={24} />
|
||||
<PencilIcon size={20} />
|
||||
{/if}
|
||||
</IconButton>
|
||||
</div>
|
||||
@@ -380,17 +252,13 @@
|
||||
{/if}
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</PageNav>
|
||||
|
||||
{#if !data.isMaterialsTabActive}
|
||||
<Exercises lessonId={data.lessonId} exerciseId={data.exerciseId} path={`${path}/exercises`} />
|
||||
{:else if !!data.lessonId}
|
||||
<PageBody
|
||||
bind:isPageNavHidden={$globalStore.isStudent}
|
||||
width="lg:w-full xl:w-11/12"
|
||||
className="overflow-x-hidden"
|
||||
>
|
||||
<PageBody isPageNavHidden={$globalStore.isStudent} width="lg:w-full xl:w-11/12" className="overflow-x-hidden">
|
||||
<!-- Shows Lesson Note / Slides / Video -->
|
||||
<Materials
|
||||
lessonId={data.lessonId}
|
||||
@@ -405,54 +273,48 @@
|
||||
|
||||
<!-- Bottom Lesson Widget -->
|
||||
<div class="absolute bottom-5 flex w-full items-center justify-center">
|
||||
<div
|
||||
class="flex w-fit items-center gap-2 rounded-full bg-gray-100 px-5 py-1 shadow-xl dark:bg-neutral-700"
|
||||
>
|
||||
<div class="flex w-fit items-center gap-2 rounded-full bg-gray-100 px-5 py-1 shadow-xl dark:bg-neutral-700">
|
||||
<button
|
||||
disabled={isPrevDisabled}
|
||||
class={`my-2 flex items-center border border-b-0 border-l-0 border-t-0 border-gray-300 px-2 pr-4 ${
|
||||
isPrevDisabled && 'cursor-not-allowed opacity-25'
|
||||
}`}
|
||||
on:click={() => goToNextOrPrevLesson(data.lessonId, true)}
|
||||
onclick={() => goToNextOrPrevLesson(data.lessonId, true)}
|
||||
>
|
||||
<ChevronLeft size={24} />
|
||||
<ChevronLeftIcon size={16} />
|
||||
|
||||
<span class="hidden md:block">{$t('course.navItem.lessons.prev')}</span>
|
||||
</button>
|
||||
{#if data.isMaterialsTabActive}
|
||||
<button
|
||||
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={() => goto(`${path}/exercises`)}
|
||||
onclick={() => goto(`${path}/exercises`)}
|
||||
>
|
||||
<ListChecked size={24} class="carbon-icon" />
|
||||
<ListChecksIcon size={16} />
|
||||
<span class="ml-1">{$lesson.totalExercises}</span>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
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={() => goto(path)}
|
||||
onclick={() => goto(path)}
|
||||
>
|
||||
<CourseIcon />
|
||||
<LibraryBigIcon size={16} />
|
||||
</button>
|
||||
{/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"
|
||||
on:click={() => markLessonComplete(data.lessonId)}
|
||||
onclick={() => markLessonComplete(data.lessonId)}
|
||||
disabled={isMarkingComplete}
|
||||
>
|
||||
{#if isLessonComplete}
|
||||
<CheckmarkFilledIcon size={24} class="carbon-icon text-primary-600" />
|
||||
{:else}
|
||||
<CheckmarkOutlineIcon size={24} class="carbon-icon" />
|
||||
{/if}
|
||||
<CircleCheckIcon filled={isLessonComplete} />
|
||||
</button>
|
||||
<button
|
||||
disabled={isNextDisabled}
|
||||
class={`my-2 flex items-center px-2 ${isNextDisabled && 'cursor-not-allowed opacity-25'}`}
|
||||
on:click={() => goToNextOrPrevLesson(data.lessonId, false)}
|
||||
onclick={() => goToNextOrPrevLesson(data.lessonId, false)}
|
||||
>
|
||||
<span class="hidden md:block">{$t('course.navItem.lessons.next')}</span>
|
||||
<ChevronRight size={24} />
|
||||
<ChevronRightIcon size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,49 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
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 { course } from '$lib/components/Course/store';
|
||||
import { CourseContainer } from '$lib/components/CourseContainer';
|
||||
import { PageBody, PageNav } from '$lib/components/Page';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { ROLE } from '$lib/utils/constants/roles';
|
||||
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 { profile } from '$lib/utils/store/user';
|
||||
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 BadgeXIcon from '@lucide/svelte/icons/badge-x';
|
||||
import DownloadIcon from '@lucide/svelte/icons/download';
|
||||
import jsPDF from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
import Papa from 'papaparse';
|
||||
|
||||
export let data;
|
||||
let { data } = $props();
|
||||
|
||||
let borderBottomGrey = 'border-r-0 border-t-0 border-b border-l-0 border-gray-300';
|
||||
let borderleftGrey = 'border-r-0 border-t-0 border-b-0 border-l border-gray-300';
|
||||
let students: GroupPerson[] = [];
|
||||
let lessonMapping = {}; // { lessonId: { exerciseId: exerciseTitle, ... }, ... }
|
||||
let studentMarksByExerciseId = {}; // { groupMemberId: { exerciseId: `total_gotten/points`, ... }, ... }
|
||||
let isDownloading = false;
|
||||
let students: GroupPerson[] = $state([]);
|
||||
let lessonMapping: Record<string, Record<string, { title: string; points: number }>> = $state({}); // { lessonId: { exerciseId: exerciseTitle, ... }, ... }
|
||||
let studentMarksByExerciseId: Record<string, Record<string, string>> = $state({}); // { groupMemberId: { exerciseId: `total_gotten/points`, ... }, ... }
|
||||
let isDownloading = $state(false);
|
||||
let hasFetched = $state(false);
|
||||
|
||||
function calculateStudentTotal(studentExerciseData) {
|
||||
if (!studentExerciseData) return 0;
|
||||
|
||||
return Object.values(studentExerciseData).reduce(
|
||||
(total: number, point) => (total += parseInt(point as string)),
|
||||
0
|
||||
);
|
||||
return Object.values(studentExerciseData).reduce((total: number, point) => (total += parseInt(point as string)), 0);
|
||||
}
|
||||
|
||||
async function firstRender(courseId: string) {
|
||||
async function firstRender(courseId?: string) {
|
||||
if (!courseId || hasFetched) return;
|
||||
|
||||
hasFetched = true;
|
||||
|
||||
const { data: marks, error } = await fetchMarks(courseId);
|
||||
if (error) {
|
||||
console.error('Error fetching marks', error);
|
||||
@@ -204,14 +202,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: students = $globalStore.isStudent
|
||||
? $group.people.filter((person) => !!person.profile && person.profile.id === $profile.id)
|
||||
: $group.people.filter((person) => !!person.profile && person.role_id === ROLE.STUDENT);
|
||||
|
||||
$: browser && $course.id && firstRender($course.id);
|
||||
$effect(() => {
|
||||
firstRender($course.id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<CourseContainer bind:courseId={data.courseId}>
|
||||
<CourseContainer courseId={data.courseId}>
|
||||
<RoleBasedSecurity
|
||||
allowedRoles={getPageRoles($currentOrg)}
|
||||
onDenied={() => {
|
||||
@@ -219,22 +215,24 @@
|
||||
}}
|
||||
>
|
||||
<PageNav title={$t('course.navItem.marks.title')}>
|
||||
<slot:fragment slot="widget">
|
||||
{#snippet widget()}
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<OverflowMenu flipped style="border-radius: 50px" class="bg-gray-100 dark:bg-neutral-800">
|
||||
<div slot="menu" style="">
|
||||
{#if isDownloading}
|
||||
<Loading withOverlay={false} small />
|
||||
{:else}
|
||||
<Download />
|
||||
{/if}
|
||||
<div style="">
|
||||
{#if isDownloading}
|
||||
<Loading withOverlay={false} small />
|
||||
{:else}
|
||||
<DownloadIcon size={16} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OverflowMenuItem text={$t('course.navItem.marks.export.csv')} on:click={downloadCSV} />
|
||||
<OverflowMenuItem text={$t('course.navItem.marks.export.pdf')} on:click={downloadPDF} />
|
||||
</OverflowMenu>
|
||||
</RoleBasedSecurity>
|
||||
</slot:fragment>
|
||||
{/snippet}
|
||||
</PageNav>
|
||||
|
||||
<PageBody width="w-full max-w-6xl md:w-11/12">
|
||||
@@ -249,9 +247,7 @@
|
||||
<p class="col lesson-number dark:text-white" title={lesson.title}>
|
||||
{getLectureNo(index + 1)}
|
||||
</p>
|
||||
<div
|
||||
class="flex h-full items-center border-b-0 border-l-0 border-r-0 border-t border-gray-300"
|
||||
>
|
||||
<div 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}
|
||||
<p
|
||||
class="col text-sm dark:text-white {index && borderleftGrey}"
|
||||
@@ -273,11 +269,7 @@
|
||||
{#each students as student}
|
||||
<div class="relative flex cursor-pointer items-center p-3 {borderBottomGrey}">
|
||||
<div class="flex w-40 items-center">
|
||||
<img
|
||||
alt="Student avatar"
|
||||
src={student.profile.avatar_url}
|
||||
class="mr-2 h-8 w-8 rounded-full"
|
||||
/>
|
||||
<img alt="Student avatar" src={student.profile.avatar_url} class="mr-2 h-8 w-8 rounded-full" />
|
||||
<div class="text-sm">
|
||||
<p class="font-semibold dark:text-white">
|
||||
{student.profile.fullname}
|
||||
@@ -311,7 +303,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<Box>
|
||||
<AudioConsoleIcon size={32} class="carbon-icon w-80" />
|
||||
<BadgeXIcon size={16} />
|
||||
<h3 class="text-3xl text-gray-500 dark:text-white">
|
||||
{$t('course.navItem.marks.no_student')}
|
||||
</h3>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import { page } from '$app/state';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import { PageBody, PageNav } from '$lib/components/Page';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import ArrowLeft from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
import { CourseContainer } from '$lib/components/CourseContainer';
|
||||
|
||||
export let data;
|
||||
let { data = $bindable(), children } = $props();
|
||||
|
||||
// Get back URL from query parameters
|
||||
$: backUrl = $page.url.searchParams.get('back');
|
||||
let backUrl = $derived(page.url.searchParams.get('back'));
|
||||
|
||||
const handleClick = () => {
|
||||
const url = $page.url.href + '?add=true';
|
||||
const url = page.url.href + '?add=true';
|
||||
goto(url);
|
||||
};
|
||||
|
||||
@@ -28,30 +28,26 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<CourseContainer bind:courseId={data.courseId}>
|
||||
<CourseContainer courseId={data.courseId}>
|
||||
<PageNav title={$t('course.navItem.people.title')} disableSticky={true}>
|
||||
<slot:fragment slot="image">
|
||||
{#snippet image()}
|
||||
{#if data.personId}
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<IconButton size="large" onClick={handleBackNavigation}>
|
||||
<ArrowLeft size={16} class="carbon-icon dark:text-white " />
|
||||
<ArrowLeftIcon size={16} />
|
||||
</IconButton>
|
||||
</RoleBasedSecurity>
|
||||
{/if}
|
||||
</slot:fragment>
|
||||
<slot:fragment slot="widget">
|
||||
{/snippet}
|
||||
{#snippet widget()}
|
||||
{#if !data.personId}
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<PrimaryButton
|
||||
className="mr-2"
|
||||
label={$t('course.navItem.people.add')}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
<PrimaryButton className="mr-2" label={$t('course.navItem.people.add')} onClick={handleClick} />
|
||||
</RoleBasedSecurity>
|
||||
{/if}
|
||||
</slot:fragment>
|
||||
{/snippet}
|
||||
</PageNav>
|
||||
<PageBody width="w-full max-w-6xl md:w-11/12">
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</PageBody>
|
||||
</CourseContainer>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
|
||||
@@ -10,7 +10,7 @@
|
||||
import type { ProfileRole } from '$lib/components/Course/components/People/types';
|
||||
import { group } from '$lib/components/Course/store';
|
||||
import Select from '$lib/components/Form/Select.svelte';
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
@@ -28,12 +28,13 @@
|
||||
StructuredListHead,
|
||||
StructuredListRow
|
||||
} from 'carbon-components-svelte';
|
||||
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
|
||||
let people: Array<GroupPerson> = [];
|
||||
let member: { id?: string; email?: string; profile?: { email: string } } = {};
|
||||
let filterBy: ProfileRole = ROLES[0];
|
||||
let searchValue = '';
|
||||
let member: { id?: string; email?: string; profile?: { email: string } } = $state({});
|
||||
let filterBy: ProfileRole = $state(ROLES[0]);
|
||||
let searchValue = $state('');
|
||||
|
||||
const people: Array<GroupPerson> = $derived(sortAndFilterPeople($group.people, filterBy));
|
||||
|
||||
function filterPeople(_query, people) {
|
||||
const query = _query.toLowerCase();
|
||||
@@ -52,16 +53,13 @@
|
||||
}
|
||||
|
||||
function sortAndFilterPeople(_people: Array<GroupPerson>, filterBy: ProfileRole) {
|
||||
people = (_people || [])
|
||||
return (_people || [])
|
||||
.filter((person) => {
|
||||
if (filterBy.value === 'all') return true;
|
||||
|
||||
return person.role_id === filterBy.value;
|
||||
})
|
||||
.sort(
|
||||
(a: GroupPerson, b: GroupPerson) =>
|
||||
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
||||
)
|
||||
.sort((a: GroupPerson, b: GroupPerson) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())
|
||||
.sort((a: GroupPerson, b: GroupPerson) => a.role_id - b.role_id);
|
||||
}
|
||||
|
||||
@@ -80,23 +78,16 @@
|
||||
}
|
||||
|
||||
function gotoPerson(person) {
|
||||
goto(`${$page.url.href}/${person.profile_id}`);
|
||||
goto(`${page.url.href}/${person.profile_id}`);
|
||||
}
|
||||
|
||||
$: sortAndFilterPeople($group.people, filterBy);
|
||||
</script>
|
||||
|
||||
<InvitationModal />
|
||||
|
||||
<DeleteConfirmation
|
||||
email={member.email || (member.profile && member.profile.email)}
|
||||
{deletePerson}
|
||||
/>
|
||||
<DeleteConfirmation email={member.email || (member.profile && member.profile.email)} {deletePerson} />
|
||||
|
||||
<section class="mx-2 my-5 md:mx-9">
|
||||
<div
|
||||
class="flex-end mb-7 flex flex-col items-start justify-end gap-2 md:flex-row md:items-center"
|
||||
>
|
||||
<div class="flex-end mb-7 flex flex-col items-start justify-end gap-2 md:flex-row md:items-center">
|
||||
<div class="max-w-[320px]">
|
||||
<Search
|
||||
class="w-full border-0 bg-zinc-100 dark:text-slate-950"
|
||||
@@ -117,14 +108,12 @@
|
||||
</select> -->
|
||||
</div>
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<p class="hidden w-20 text-lg dark:text-white lg:block" />
|
||||
<p class="hidden w-20 text-lg lg:block dark:text-white"></p>
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
|
||||
<StructuredList class="m-0">
|
||||
<StructuredListHead
|
||||
class="bg-slate-100 dark:border-2 dark:border-neutral-800 dark:bg-neutral-800"
|
||||
>
|
||||
<StructuredListHead class="bg-slate-100 dark:border-2 dark:border-neutral-800 dark:bg-neutral-800">
|
||||
<StructuredListRow head class="mx-7">
|
||||
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
|
||||
>{$t('course.navItem.people.name')}</StructuredListCell
|
||||
@@ -136,7 +125,7 @@
|
||||
>{$t('course.navItem.people.action')}</StructuredListCell
|
||||
>
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<p class="hidden w-20 text-lg dark:text-white lg:block" />
|
||||
<p class="hidden w-20 text-lg lg:block dark:text-white"></p>
|
||||
</RoleBasedSecurity>
|
||||
</StructuredListRow>
|
||||
</StructuredListHead>
|
||||
@@ -181,18 +170,12 @@
|
||||
className="bg-primary-200 text-black font-semibold text-xs mr-3"
|
||||
shape="rounded-full"
|
||||
/>
|
||||
<a
|
||||
href="mailto:{person.email}"
|
||||
class="text-md text-primary-600 mr-2 dark:text-white"
|
||||
>
|
||||
<a href="mailto:{person.email}" class="text-md text-primary-600 mr-2 dark:text-white">
|
||||
{person.email}
|
||||
</a>
|
||||
<div class="flex items-center justify-between">
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<CopyButton
|
||||
text={getEmail(person)}
|
||||
feedback={$t('course.navItem.people.feedback')}
|
||||
/>
|
||||
<CopyButton text={getEmail(person)} feedback={$t('course.navItem.people.feedback')} />
|
||||
</RoleBasedSecurity>
|
||||
|
||||
<TextChip
|
||||
@@ -223,12 +206,12 @@
|
||||
$deleteMemberModal.open = true;
|
||||
}}
|
||||
>
|
||||
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
<!-- <IconButton
|
||||
onClick={() => gotoPerson(person)}
|
||||
>
|
||||
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
|
||||
<TrashIcon size={16} />
|
||||
</IconButton> -->
|
||||
|
||||
<PrimaryButton
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import type { UserCourseAnalytics } from '$lib/utils/types/analytics';
|
||||
import { Grid, Tag } from 'carbon-components-svelte';
|
||||
import Notebook from 'carbon-icons-svelte/lib/Notebook.svelte';
|
||||
import Report from 'carbon-icons-svelte/lib/Report.svelte';
|
||||
import RowExpand from 'carbon-icons-svelte/lib/RowExpand.svelte';
|
||||
import BookOpenIcon from '@lucide/svelte/icons/book-open';
|
||||
import ChartLineIcon from '@lucide/svelte/icons/chart-line';
|
||||
import UnfoldVerticalIcon from '@lucide/svelte/icons/unfold-vertical';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
export let data;
|
||||
let { data } = $props();
|
||||
|
||||
let userCourseAnalytics: UserCourseAnalytics;
|
||||
let userCourseAnalytics: UserCourseAnalytics | undefined = $state();
|
||||
|
||||
function getPercentage(a: number, b: number): number {
|
||||
if (b === 0) {
|
||||
@@ -23,7 +23,7 @@
|
||||
return Math.round((a / b) * 100);
|
||||
}
|
||||
|
||||
let exerciseFilter: 'all' | 'completed' | 'incomplete' = 'all';
|
||||
let exerciseFilter: 'all' | 'completed' | 'incomplete' = $state('all');
|
||||
function toggleExerciseFilter(filter: 'completed' | 'incomplete') {
|
||||
if (exerciseFilter === filter) {
|
||||
exerciseFilter = 'all';
|
||||
@@ -55,42 +55,44 @@
|
||||
fetchUserCourseAnalytics();
|
||||
});
|
||||
|
||||
$: learningActivities = [
|
||||
let learningActivities = $derived([
|
||||
{
|
||||
description: $t('analytics.overall_course_progress_user_description'),
|
||||
icon: Notebook,
|
||||
icon: BookOpenIcon,
|
||||
percentage: userCourseAnalytics?.progressPercentage,
|
||||
title: $t('analytics.overall_course_progress')
|
||||
},
|
||||
{
|
||||
description: $t('analytics.assignment_completion_description'),
|
||||
icon: Report,
|
||||
icon: ChartLineIcon,
|
||||
percentage: getPercentage(
|
||||
userCourseAnalytics?.userExercisesStats?.filter((exercise) => exercise.isCompleted)?.length,
|
||||
userCourseAnalytics?.userExercisesStats?.length
|
||||
userCourseAnalytics?.userExercisesStats?.filter((exercise) => exercise.isCompleted)?.length || 0,
|
||||
userCourseAnalytics?.userExercisesStats?.length || 0
|
||||
),
|
||||
title: $t('analytics.assignment_completion')
|
||||
},
|
||||
{
|
||||
description: $t('analytics.average_grade_description'),
|
||||
icon: RowExpand,
|
||||
icon: UnfoldVerticalIcon,
|
||||
percentage: userCourseAnalytics?.averageGrade,
|
||||
title: $t('analytics.average_grade')
|
||||
}
|
||||
];
|
||||
]);
|
||||
|
||||
$: filteredExercises = userCourseAnalytics?.userExercisesStats?.filter((exercise) => {
|
||||
if (exerciseFilter === 'all') {
|
||||
return true;
|
||||
}
|
||||
return exercise.isCompleted === (exerciseFilter === 'completed');
|
||||
});
|
||||
$: completedExercises = userCourseAnalytics?.userExercisesStats?.filter(
|
||||
(exercise) => exercise.isCompleted
|
||||
)?.length;
|
||||
$: incompleteExercises = userCourseAnalytics?.userExercisesStats?.filter(
|
||||
(exercise) => !exercise.isCompleted
|
||||
)?.length;
|
||||
let filteredExercises = $derived(
|
||||
userCourseAnalytics?.userExercisesStats?.filter((exercise) => {
|
||||
if (exerciseFilter === 'all') {
|
||||
return true;
|
||||
}
|
||||
return exercise.isCompleted === (exerciseFilter === 'completed');
|
||||
})
|
||||
);
|
||||
let completedExercises = $derived(
|
||||
userCourseAnalytics?.userExercisesStats?.filter((exercise) => exercise.isCompleted)?.length || 0
|
||||
);
|
||||
let incompleteExercises = $derived(
|
||||
userCourseAnalytics?.userExercisesStats?.filter((exercise) => !exercise.isCompleted)?.length || 0
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if userCourseAnalytics}
|
||||
@@ -122,9 +124,7 @@
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<Progress
|
||||
value={getPercentage(completedExercises, userCourseAnalytics?.userExercisesStats?.length)}
|
||||
/>
|
||||
<Progress value={getPercentage(completedExercises, userCourseAnalytics?.userExercisesStats?.length)} />
|
||||
<div class="flex items-center justify-between">
|
||||
<Tag
|
||||
interactive
|
||||
@@ -160,7 +160,7 @@
|
||||
transition:fade={{ duration: 300 }}
|
||||
>
|
||||
<div class="flex w-2/3 items-center gap-4">
|
||||
<svelte:component this={Notebook} size={24} class="text-black" />
|
||||
<BookOpenIcon size={16} />
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<a
|
||||
@@ -189,13 +189,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tag
|
||||
class={`${
|
||||
exercise.isCompleted
|
||||
? 'bg-green-200 text-green-700'
|
||||
: 'bg-yellow-200 text-yellow-700'
|
||||
}`}
|
||||
>
|
||||
<Tag class={`${exercise.isCompleted ? 'bg-green-200 text-green-700' : 'bg-yellow-200 text-yellow-700'}`}>
|
||||
{exercise.isCompleted ? $t('analytics.completed') : $t('analytics.incomplete')}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
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 { CourseContainer } from '$lib/components/CourseContainer';
|
||||
|
||||
export let data;
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<CourseContainer bind:courseId={data.courseId}>
|
||||
<CourseContainer courseId={data.courseId}>
|
||||
<PageNav title={$t('course.navItem.settings.heading')} />
|
||||
<PageBody>
|
||||
<Settings />
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import Chip from '$lib/components/Chip/index.svelte';
|
||||
import MarkExerciseModal from '$lib/components/Course/components/Lesson/Exercise/MarkExerciseModal.svelte';
|
||||
import { formatAnswers } from '$lib/components/Course/function.js';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
|
||||
import { CourseContainer } from '$lib/components/CourseContainer';
|
||||
import { PageBody, PageNav } from '$lib/components/Page';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import formatDate from '$lib/utils/functions/formatDate';
|
||||
import isSubmissionEarly from '$lib/utils/functions/isSubmissionEarly';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import {
|
||||
NOTIFICATION_NAME,
|
||||
triggerSendEmail
|
||||
} from '$lib/utils/services/notification/notification';
|
||||
import { NOTIFICATION_NAME, triggerSendEmail } from '$lib/utils/services/notification/notification';
|
||||
import {
|
||||
deleteSubmission,
|
||||
fetchSubmissions,
|
||||
@@ -24,16 +20,12 @@
|
||||
updateSubmission
|
||||
} from '$lib/utils/services/submissions';
|
||||
import { currentOrg, currentOrgDomain } from '$lib/utils/store/org';
|
||||
import type {
|
||||
SubmissionIdData,
|
||||
SubmissionItem,
|
||||
SubmissionSection
|
||||
} from '$lib/utils/types/submission';
|
||||
import type { SubmissionIdData, SubmissionItem, SubmissionSection } 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;
|
||||
let { data = $bindable() } = $props();
|
||||
const { courseId } = data;
|
||||
|
||||
const flipDurationMs = 300;
|
||||
@@ -41,12 +33,16 @@
|
||||
let lessonDetails: { id: string; title: string };
|
||||
let totalMark = 0;
|
||||
let maxMark = 0;
|
||||
let submissionIdData: { [key: number]: SubmissionIdData } = {};
|
||||
let submissionId: string;
|
||||
let openExercise = false;
|
||||
let isGradeWithAI = false;
|
||||
let fetching = false;
|
||||
let isSaving = false;
|
||||
let submissionIdData: { [key: number]: SubmissionIdData } = $state({});
|
||||
let isGradeWithAI = $state(false);
|
||||
let fetching = $state(false);
|
||||
let isSaving = $state(false);
|
||||
let hasFetched = $state(false);
|
||||
|
||||
const submissionId = $derived(new URLSearchParams(page.url.search).get('submissionId') ?? '');
|
||||
let openExercise = $derived.by(() => {
|
||||
return !!submissionId && !!submissionIdData[submissionId];
|
||||
});
|
||||
|
||||
const submissionStatus: { [key: number]: string } = {
|
||||
1: $t('course.navItem.submissions.submission_status.submitted'),
|
||||
@@ -54,7 +50,7 @@
|
||||
3: $t('course.navItem.submissions.submission_status.graded')
|
||||
};
|
||||
|
||||
let sections: SubmissionSection[] = [
|
||||
let sections: SubmissionSection[] = $state([
|
||||
{
|
||||
id: 1,
|
||||
title: $t('course.navItem.submissions.submission_status.submitted'),
|
||||
@@ -73,7 +69,7 @@
|
||||
value: 10,
|
||||
items: []
|
||||
}
|
||||
];
|
||||
]);
|
||||
|
||||
function getMaxPoints(questions) {
|
||||
return (questions || []).reduce((acc, question) => acc + question.points, 0);
|
||||
@@ -163,7 +159,7 @@
|
||||
|
||||
function handleModalClose() {
|
||||
isGradeWithAI = false;
|
||||
goto($page.url.pathname);
|
||||
goto(page.url.pathname);
|
||||
}
|
||||
|
||||
// Via dialog
|
||||
@@ -238,11 +234,7 @@
|
||||
handleModalClose();
|
||||
}
|
||||
|
||||
async function handleSave(submission: {
|
||||
questionAnswerByPoint: any;
|
||||
questionAnswers: any;
|
||||
feedback: any;
|
||||
}) {
|
||||
async function handleSave(submission: { questionAnswerByPoint: any; questionAnswers: any; feedback: any }) {
|
||||
isSaving = true;
|
||||
const { questionAnswerByPoint, questionAnswers, feedback } = submission;
|
||||
|
||||
@@ -279,7 +271,11 @@
|
||||
isSaving = false;
|
||||
}
|
||||
|
||||
async function firstRender(courseId: string) {
|
||||
async function firstRender(courseId?: string) {
|
||||
if (!courseId || hasFetched) return;
|
||||
|
||||
hasFetched = true;
|
||||
|
||||
fetching = true;
|
||||
const { data: submissions } = await fetchSubmissions(courseId);
|
||||
const sectionById: { [key: number]: SubmissionSection[] } = {};
|
||||
@@ -345,15 +341,13 @@
|
||||
value: Array.isArray(sectionById[index + 1]) ? sectionById[index + 1].length : 0,
|
||||
items: Array.isArray(sectionById[index + 1]) ? sectionById[index + 1] : []
|
||||
}));
|
||||
|
||||
fetching = false;
|
||||
}
|
||||
|
||||
$: browser && $course.id && firstRender($course.id);
|
||||
$: {
|
||||
const query = new URLSearchParams($page.url.search);
|
||||
submissionId = query.get('submissionId') ?? '';
|
||||
openExercise = !!submissionId && submissionIdData[submissionId];
|
||||
}
|
||||
$effect(() => {
|
||||
firstRender($course.id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<MarkExerciseModal
|
||||
@@ -367,7 +361,7 @@
|
||||
{isSaving}
|
||||
/>
|
||||
|
||||
<CourseContainer bind:courseId={data.courseId}>
|
||||
<CourseContainer courseId={data.courseId}>
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<PageNav title={$t('course.navItem.submissions.title')} />
|
||||
|
||||
@@ -394,8 +388,8 @@
|
||||
flipDurationMs,
|
||||
dropTargetStyle: { outline: 'blue' }
|
||||
}}
|
||||
on:consider={handleDndConsiderCards(idx)}
|
||||
on:finalize={handleDndFinalizeCards(idx)}
|
||||
onconsider={handleDndConsiderCards(idx)}
|
||||
onfinalize={handleDndFinalizeCards(idx)}
|
||||
>
|
||||
{#each items as item (item.id)}
|
||||
<div
|
||||
@@ -406,27 +400,20 @@
|
||||
>
|
||||
<a
|
||||
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
|
||||
alt="Student avatar"
|
||||
class="block h-6 w-6 rounded-full"
|
||||
src={item.student.avatar_url}
|
||||
/>
|
||||
<img alt="Student avatar" class="block h-6 w-6 rounded-full" src={item.student.avatar_url} />
|
||||
<p class="ml-2 text-sm dark:text-white">
|
||||
{item.student.username}
|
||||
</p>
|
||||
</a>
|
||||
<a
|
||||
class="text-primary-700 text-md font-bold"
|
||||
href="{$page.url.pathname}?submissionId={item.id}"
|
||||
>
|
||||
<a class="text-primary-700 text-md font-bold" href="{page.url.pathname}?submissionId={item.id}">
|
||||
{item.exercise.title}
|
||||
</a>
|
||||
<a
|
||||
class="my-2 flex items-center text-black no-underline hover:underline"
|
||||
href="{$page.url?.pathname?.replace('submissions', 'lessons')}/{item.lesson
|
||||
.id}/exercises/{item.exercise.id}"
|
||||
href="{page.url?.pathname?.replace('submissions', 'lessons')}/{item.lesson.id}/exercises/{item
|
||||
.exercise.id}"
|
||||
>
|
||||
<p class="text-grey text-sm dark:text-white">
|
||||
#{item.lesson.title}
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
import EmailSentIcon from '$lib/components/Icons/EmailSentIcon.svelte';
|
||||
|
||||
let supabase = getSupabase();
|
||||
let fields = Object.assign({}, FORGOT_PASSWORD_FIELDS);
|
||||
let submitError;
|
||||
let loading = false;
|
||||
let success = false;
|
||||
let errors = {};
|
||||
let fields = $state(Object.assign({}, FORGOT_PASSWORD_FIELDS));
|
||||
let submitError = $state();
|
||||
let loading = $state(false);
|
||||
let success = $state(false);
|
||||
let errors = $state({});
|
||||
|
||||
async function handleSubmit() {
|
||||
errors = forgotValidation(fields);
|
||||
@@ -48,27 +48,22 @@
|
||||
|
||||
<AuthUI {supabase} {handleSubmit} showOnlyContent={true} showLogo={!success}>
|
||||
{#if success}
|
||||
<div class="mt-4 w-full flex items-center justify-center flex-col">
|
||||
<EmailSentIcon />
|
||||
<h3 class="dark:text-white text-xl font-semibold my-3">Email Sent!</h3>
|
||||
<p class="dark:text-white text-md mb-6 text-center">
|
||||
We have sent a confirmation email to <span class="text-primary-700">{fields.email}</span>.
|
||||
Kindly check your inbox to reset password or spam to reset your password.
|
||||
<div class="mt-4 flex w-full flex-col items-center justify-center">
|
||||
<EmailSentIcon size={16} />
|
||||
<h3 class="my-3 text-xl font-semibold dark:text-white">Email Sent!</h3>
|
||||
<p class="text-md mb-6 text-center dark:text-white">
|
||||
We have sent a confirmation email to <span class="text-primary-700">{fields.email}</span>. Kindly check your
|
||||
inbox to reset password or spam to reset your password.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="my-4 w-full flex justify-end items-center flex-col">
|
||||
<PrimaryButton
|
||||
label="Okay"
|
||||
type="button"
|
||||
className="sm:w-full w-full mb-4"
|
||||
onClick={() => goto(ROUTE.LOGIN)}
|
||||
/>
|
||||
<div class="my-4 flex w-full flex-col items-center justify-end">
|
||||
<PrimaryButton label="Okay" type="button" className="sm:w-full w-full mb-4" onClick={() => goto(ROUTE.LOGIN)} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-4 w-full">
|
||||
<h3 class="dark:text-white text-xl font-semibold my-3">Forgot Password</h3>
|
||||
<p class="dark:text-white text-sm mb-6">We will send you a reset link to your email</p>
|
||||
<h3 class="my-3 text-xl font-semibold dark:text-white">Forgot Password</h3>
|
||||
<p class="mb-6 text-sm dark:text-white">We will send you a reset link to your email</p>
|
||||
<TextField
|
||||
label="Your email"
|
||||
bind:value={fields.email}
|
||||
@@ -85,7 +80,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="my-4 w-full flex justify-end items-center flex-col">
|
||||
<div class="my-4 flex w-full flex-col items-center justify-end">
|
||||
<PrimaryButton
|
||||
label="Reset Password"
|
||||
type="submit"
|
||||
|
||||
@@ -26,6 +26,6 @@ export const load = async ({ params = { hash: '' } }) => {
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error decoding course invite params.hash', error);
|
||||
throw redirect(307, '/404');
|
||||
redirect(307, '/404');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,21 +10,18 @@
|
||||
import type { CurrentOrg } from '$lib/utils/types/org.js';
|
||||
import { ROLE } from '$lib/utils/constants/roles';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import {
|
||||
triggerSendEmail,
|
||||
NOTIFICATION_NAME
|
||||
} from '$lib/utils/services/notification/notification';
|
||||
import { triggerSendEmail, NOTIFICATION_NAME } from '$lib/utils/services/notification/notification';
|
||||
import { snackbar } from '$lib/components/Snackbar/store.js';
|
||||
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
export let data;
|
||||
let { data } = $props();
|
||||
|
||||
let supabase = getSupabase();
|
||||
let loading = false;
|
||||
let loading = $state(false);
|
||||
|
||||
let disableSubmit = false;
|
||||
let formRef: HTMLFormElement;
|
||||
let formRef: HTMLFormElement = $state();
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
@@ -34,11 +31,7 @@
|
||||
return goto(`/signup?redirect=${$page.url?.pathname || ''}`);
|
||||
}
|
||||
|
||||
const { data: courseData, error } = await supabase
|
||||
.from('course')
|
||||
.select('group_id')
|
||||
.eq('id', data.id)
|
||||
.single();
|
||||
const { data: courseData, error } = await supabase.from('course').select('group_id').eq('id', data.id).single();
|
||||
|
||||
console.log({ courseData });
|
||||
if (!courseData?.group_id) {
|
||||
@@ -118,9 +111,8 @@
|
||||
|
||||
onMount(async () => {
|
||||
setTheme(data.currentOrg?.theme || '');
|
||||
setCurOrg(data.currentOrg as CurrentOrg);
|
||||
});
|
||||
|
||||
$: setCurOrg(data.currentOrg as CurrentOrg);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
||||
@@ -31,6 +31,6 @@ export const load = async ({ params = { hash: '' } }) => {
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error decoding invite params.hash', error);
|
||||
throw redirect(307, '/404');
|
||||
redirect(307, '/404');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import AuthUI from '$lib/components/AuthUI/index.svelte';
|
||||
import type { Profile } from '$lib/components/Course/components/People/types';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
@@ -12,34 +11,31 @@
|
||||
import { setTheme } from '$lib/utils/functions/theme';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { profile, user } from '$lib/utils/store/user';
|
||||
import {
|
||||
authValidation,
|
||||
getConfirmPasswordError,
|
||||
getDisableSubmit
|
||||
} from '$lib/utils/functions/validator';
|
||||
import { authValidation, getConfirmPasswordError, getDisableSubmit } from '$lib/utils/functions/validator';
|
||||
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
||||
import type { CurrentOrg } from '$lib/utils/types/org';
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { snackbar } from '$lib/components/Snackbar/store.js';
|
||||
|
||||
export let data;
|
||||
let { data } = $props();
|
||||
|
||||
let supabase = getSupabase();
|
||||
let fields = Object.assign({}, SIGNUP_FIELDS);
|
||||
let loading = false;
|
||||
let isLoggingOut = false;
|
||||
let shouldLogout = false;
|
||||
let fields = $state(Object.assign({}, SIGNUP_FIELDS));
|
||||
let loading = $state(false);
|
||||
let isLoggingOut = $state(false);
|
||||
let shouldLogout = $state(false);
|
||||
|
||||
let errors: {
|
||||
name?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
confirmPassword?: string;
|
||||
} = {};
|
||||
} = $state({});
|
||||
|
||||
let submitError: string;
|
||||
let disableSubmit = false;
|
||||
let formRef: HTMLFormElement;
|
||||
let submitError: string = $state('');
|
||||
let formRef: HTMLFormElement | undefined = $state();
|
||||
|
||||
const confirmPasswordError = $derived(getConfirmPasswordError(fields));
|
||||
const disableSubmit = $derived(getDisableSubmit(fields));
|
||||
|
||||
async function joinOrg(profileId: string, email: string) {
|
||||
if (!profileId || !email || !data.invite.currentOrg?.id) return;
|
||||
@@ -163,23 +159,20 @@
|
||||
setCurOrg(data.invite.currentOrg as CurrentOrg);
|
||||
});
|
||||
|
||||
$: errors.confirmPassword = getConfirmPasswordError(fields);
|
||||
$: disableSubmit = getDisableSubmit(fields);
|
||||
const isLoading = $derived(loading || $user.fetchingUser);
|
||||
|
||||
$: autoLogout($profile?.email);
|
||||
function autoLogout(email?: string) {
|
||||
$effect(() => {
|
||||
const email = $profile?.email;
|
||||
if (!email) return;
|
||||
|
||||
if (email !== data.invite.email) {
|
||||
console.log('logout');
|
||||
snackbar.error('You are logged in with a different email');
|
||||
shouldLogout = true;
|
||||
untrack(() => {
|
||||
shouldLogout = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$: isLoading = loading || $user.fetchingUser;
|
||||
$: console.log('$profile', $profile);
|
||||
$: console.log('data.invite', data.invite);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -188,7 +181,7 @@
|
||||
|
||||
<AuthUI
|
||||
{supabase}
|
||||
redirectPathname={$page.url.pathname}
|
||||
redirectPathname={page.url.pathname}
|
||||
isLogin={false}
|
||||
{handleSubmit}
|
||||
{isLoading}
|
||||
@@ -248,7 +241,7 @@
|
||||
className="mb-6"
|
||||
inputClassName="w-full"
|
||||
isDisabled={isLoading}
|
||||
errorMessage={errors.confirmPassword}
|
||||
errorMessage={confirmPasswordError}
|
||||
isRequired
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import LMSSideBar from '$lib/components/LMS/SideBar.svelte';
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="org-root w-full flex items-center justify-between">
|
||||
<div class="org-root flex w-full items-center justify-between">
|
||||
<LMSSideBar />
|
||||
<div class="org-slot bg-white dark:bg-black w-full">
|
||||
<slot />
|
||||
<div class="org-slot w-full bg-white dark:bg-black">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,41 +10,39 @@
|
||||
import { fetchCourses } from '$lib/utils/services/courses';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
let hasFetched = false;
|
||||
let progressPercentage = 0;
|
||||
let totalLessons = 0;
|
||||
let totalCompleted = 0;
|
||||
|
||||
async function getCourses(userId: string | undefined, orgId: string) {
|
||||
let totalCompleted = $derived($courses.reduce((acc, cur) => acc + (cur.progress_rate || 0), 0));
|
||||
let totalLessons = $derived($courses.reduce((acc, cur) => acc + (cur.total_lessons || 0), 0));
|
||||
let progressPercentage = $derived(Math.round((totalCompleted / totalLessons) * 100) || 0);
|
||||
|
||||
function getCourses(userId?: string, orgId?: string) {
|
||||
if (hasFetched || !userId || !orgId) {
|
||||
return;
|
||||
}
|
||||
// only show is loading when fetching for the first time
|
||||
if (!$courses.length) {
|
||||
$courseMetaDeta.isLoading = true;
|
||||
}
|
||||
|
||||
const coursesResult = await fetchCourses(userId, orgId);
|
||||
console.log(`coursesResult`, coursesResult);
|
||||
untrack(async () => {
|
||||
// only show is loading when fetching for the first time
|
||||
if (!$courses.length) {
|
||||
$courseMetaDeta.isLoading = true;
|
||||
}
|
||||
|
||||
$courseMetaDeta.isLoading = false;
|
||||
if (!coursesResult) return;
|
||||
const coursesResult = await fetchCourses(userId, orgId);
|
||||
console.log(`coursesResult`, coursesResult);
|
||||
|
||||
courses.set(coursesResult.allCourses);
|
||||
hasFetched = true;
|
||||
$courseMetaDeta.isLoading = false;
|
||||
if (!coursesResult) return;
|
||||
|
||||
courses.set(coursesResult.allCourses);
|
||||
hasFetched = true;
|
||||
});
|
||||
}
|
||||
|
||||
function calcTotalProgress(courses: Course[]) {
|
||||
totalCompleted = courses.reduce((acc, cur) => acc + (cur.progress_rate || 0), 0);
|
||||
totalLessons = courses.reduce((acc, cur) => acc + (cur.total_lessons || 0), 0);
|
||||
|
||||
progressPercentage = Math.round((totalCompleted / totalLessons) * 100) || 0;
|
||||
}
|
||||
|
||||
$: getCourses($profile.id, $currentOrg.id);
|
||||
$: calcTotalProgress($courses);
|
||||
$effect(() => {
|
||||
getCourses($profile.id, $currentOrg.id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -54,7 +52,7 @@
|
||||
<section class="mx-auto max-w-6xl gap-5">
|
||||
<div class="m-5">
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold dark:text-white md:text-3xl">
|
||||
<h1 class="text-2xl font-bold md:text-3xl dark:text-white">
|
||||
{$t(getGreeting())}
|
||||
{$profile.fullname}!
|
||||
</h1>
|
||||
@@ -94,7 +92,7 @@
|
||||
{$t('dashboard.your_progress')}
|
||||
</p>
|
||||
<div
|
||||
class="flex h-fit items-center justify-center gap-2 rounded border border-[#EAEAEA] p-3 dark:bg-neutral-800 lg:h-[40vh] lg:overflow-y-auto"
|
||||
class="flex h-fit items-center justify-center gap-2 rounded border border-[#EAEAEA] p-3 lg:h-[40vh] lg:overflow-y-auto dark:bg-neutral-800"
|
||||
>
|
||||
<div
|
||||
class="flex h-full w-full flex-col items-center justify-between gap-5 sm:flex-row lg:items-center lg:justify-around xl:flex-col xl:items-start"
|
||||
@@ -117,9 +115,7 @@
|
||||
</p>
|
||||
{/if}
|
||||
</span>
|
||||
<h1
|
||||
class="my-0 whitespace-nowrap text-5xl font-bold text-[#262626] dark:text-white lg:text-6xl"
|
||||
>
|
||||
<h1 class="my-0 whitespace-nowrap text-5xl font-bold text-[#262626] lg:text-6xl dark:text-white">
|
||||
{progressPercentage} %
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
<title>Community - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full max-w-4xl mx-auto">
|
||||
<div class="py-10 px-5">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">{$t('community.title')}</h1>
|
||||
<section class="mx-auto w-full max-w-4xl">
|
||||
<div class="px-5 py-10">
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-3xl font-bold dark:text-white">{$t('community.title')}</h1>
|
||||
<PrimaryButton label={$t('community.ask_button')} onClick={askCommunity} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,34 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import pluralize from 'pluralize';
|
||||
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import { Dropdown, SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
|
||||
import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
import Vote from '$lib/components/Vote/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
import { currentOrg, isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import {
|
||||
askCommunityValidation,
|
||||
commentInCommunityValidation
|
||||
} from '$lib/utils/functions/validator';
|
||||
import { askCommunityValidation, commentInCommunityValidation } from '$lib/utils/functions/validator';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import DeleteModal from '$lib/components/Org/Community/DeleteModal.svelte';
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { browser } from '$app/environment';
|
||||
import { untrack } from 'svelte';
|
||||
import { fetchCourses } from '$lib/utils/services/courses';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { courses } from '$lib/components/Courses/store';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
|
||||
export let data;
|
||||
let { data } = $props();
|
||||
const { slug } = data;
|
||||
|
||||
interface Comment {
|
||||
@@ -56,39 +53,38 @@
|
||||
courseId: string;
|
||||
}
|
||||
|
||||
let question: Question;
|
||||
let comment = '';
|
||||
let question: Question | undefined = $state();
|
||||
let comment = $state('');
|
||||
let errors: {
|
||||
title?: string;
|
||||
courseId?: string;
|
||||
} = {};
|
||||
} = $state({});
|
||||
let isValidAnswer = false; // V2 allow admin mark an answer as accepted
|
||||
let resetInput = 1;
|
||||
let voted: {
|
||||
question: boolean;
|
||||
comment: {
|
||||
[key: string]: boolean;
|
||||
};
|
||||
} = { question: false, comment: {} };
|
||||
let isEditMode = false;
|
||||
let deleteComment = {
|
||||
} = $state({ question: false, comment: {} });
|
||||
let isEditMode = $state(false);
|
||||
let deleteComment = $state({
|
||||
shouldDelete: false,
|
||||
commentId: '',
|
||||
isDeleting: false
|
||||
};
|
||||
let deleteQuestion = {
|
||||
});
|
||||
let deleteQuestion = $state({
|
||||
shouldDelete: false,
|
||||
questionId: '',
|
||||
isDeleting: false
|
||||
};
|
||||
let editContent = {
|
||||
});
|
||||
let editContent = $state({
|
||||
title: '',
|
||||
body: '',
|
||||
courseId: ''
|
||||
};
|
||||
});
|
||||
|
||||
let editorInstance = false;
|
||||
let fetchedCourses: Course[] = [];
|
||||
let editorInstance = $state(false);
|
||||
let fetchedCourses: Course[] = $state([]);
|
||||
|
||||
function mapResToQuestion(data): Question {
|
||||
return {
|
||||
@@ -161,11 +157,15 @@
|
||||
return goto(`/lms`);
|
||||
}
|
||||
|
||||
question = mapResToQuestion(data);
|
||||
question.totalComments = question.comments.length;
|
||||
untrack(() => {
|
||||
question = mapResToQuestion(data);
|
||||
question.totalComments = question.comments.length;
|
||||
});
|
||||
}
|
||||
|
||||
async function submitComment() {
|
||||
if (!question) return;
|
||||
|
||||
errors = commentInCommunityValidation({ comment });
|
||||
console.log('submitComment errors', errors);
|
||||
|
||||
@@ -209,11 +209,12 @@
|
||||
|
||||
// Reset input
|
||||
comment = '';
|
||||
resetInput = new Date().getTime();
|
||||
}
|
||||
}
|
||||
|
||||
async function upvoteQuestion(type: string, commentId?: string) {
|
||||
if (!question) return;
|
||||
|
||||
const isQuestion = type === 'question';
|
||||
|
||||
if (isQuestion && voted.question) return;
|
||||
@@ -249,6 +250,8 @@
|
||||
}
|
||||
|
||||
async function handleQuestionEdit() {
|
||||
if (!question) return;
|
||||
|
||||
if (isEditMode) {
|
||||
errors = askCommunityValidation(editContent);
|
||||
console.log('handleQuestionEdit errors', errors);
|
||||
@@ -296,13 +299,12 @@
|
||||
}
|
||||
|
||||
async function handleDelete(isQuestion: boolean) {
|
||||
if (!question) return;
|
||||
|
||||
if (!isQuestion) {
|
||||
deleteComment.isDeleting = true;
|
||||
|
||||
const { error } = await supabase
|
||||
.from('community_answer')
|
||||
.delete()
|
||||
.match({ id: deleteComment.commentId });
|
||||
const { error } = await supabase.from('community_answer').delete().match({ id: deleteComment.commentId });
|
||||
|
||||
deleteComment.isDeleting = false;
|
||||
|
||||
@@ -351,12 +353,14 @@
|
||||
deleteQuestion.isDeleting = false;
|
||||
}
|
||||
|
||||
$: browser && fetchCommunityQuestion(slug);
|
||||
$: {
|
||||
$effect(() => {
|
||||
fetchCommunityQuestion(slug);
|
||||
});
|
||||
$effect(() => {
|
||||
if ($profile.id && $currentOrg.id) {
|
||||
getCourses($profile.id, $currentOrg.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -365,7 +369,7 @@
|
||||
|
||||
<DeleteModal
|
||||
bind:open={deleteQuestion.shouldDelete}
|
||||
bind:isDeleting={deleteQuestion.isDeleting}
|
||||
isDeleting={deleteQuestion.isDeleting}
|
||||
onCancel={() => {
|
||||
deleteQuestion.shouldDelete = false;
|
||||
deleteQuestion.questionId = '';
|
||||
@@ -383,28 +387,24 @@
|
||||
}}
|
||||
onDelete={() => handleDelete(false)}
|
||||
/>
|
||||
<section class="max-w-3xl mx-auto md:mx-10 lg:mb-20">
|
||||
<section class="mx-auto max-w-3xl md:mx-10 lg:mb-20">
|
||||
{#if !question}
|
||||
<div class="py-10 px-5 mb-3">
|
||||
<div class="mb-3 px-5 py-10">
|
||||
<SkeletonText style="width: 25%;" />
|
||||
<SkeletonText style="width: 100%; margin-bottom: 2rem" />
|
||||
<SkeletonPlaceholder style="width: 100%; height: 20rem;" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="py-10 px-5">
|
||||
<a class="text-gray-500 dark:text-white text-md flex items-center" href={`/lms/community`}>
|
||||
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" />
|
||||
<div class="px-5 py-10">
|
||||
<a class="text-md flex items-center text-gray-500 dark:text-white" href="/lms/community">
|
||||
<ArrowLeftIcon size={16} />
|
||||
{$t('community.ask.go_back')}
|
||||
</a>
|
||||
<div class="my-5 flex justify-between items-center">
|
||||
<div class="my-5 flex items-center justify-between">
|
||||
{#if isEditMode}
|
||||
<TextField
|
||||
bind:value={editContent.title}
|
||||
className="w-full mr-2"
|
||||
errorMessage={errors.title}
|
||||
/>
|
||||
<TextField bind:value={editContent.title} className="w-full mr-2" errorMessage={errors.title} />
|
||||
<Dropdown
|
||||
class="w-[25%] h-full"
|
||||
class="h-full w-[25%]"
|
||||
size="xl"
|
||||
label="Select Course"
|
||||
items={fetchedCourses.map((course) => ({ id: course.id, text: course.title }))}
|
||||
@@ -412,11 +412,7 @@
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex items-center">
|
||||
<Vote
|
||||
value={question.votes}
|
||||
upVote={() => upvoteQuestion('question')}
|
||||
disabled={voted.question}
|
||||
/>
|
||||
<Vote value={question.votes} upVote={() => upvoteQuestion('question')} disabled={voted.question} />
|
||||
<h2 class="text-3xl">{question.title}</h2>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -439,17 +435,12 @@
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="my-1 px-1 rounded-lg border border-1 border-gray">
|
||||
<header class="flex items-center justify-between leading-none p-2">
|
||||
<div class="flex items-center no-underline hover:underline text-black">
|
||||
<Avatar
|
||||
src={question.author.avatar}
|
||||
name={question.author.name}
|
||||
width="w-7"
|
||||
height="h-7"
|
||||
/>
|
||||
<p class="dark:text-white ml-2 text-sm">{question.author.name}</p>
|
||||
<p class="dark:text-white ml-2 text-sm text-gray-500">
|
||||
<div class="border-1 border-gray my-1 rounded-lg border px-1">
|
||||
<header class="flex items-center justify-between p-2 leading-none">
|
||||
<div class="flex items-center text-black no-underline hover:underline">
|
||||
<Avatar src={question.author.avatar} name={question.author.name} width="w-7" height="h-7" />
|
||||
<p class="ml-2 text-sm dark:text-white">{question.author.name}</p>
|
||||
<p class="ml-2 text-sm text-gray-500 dark:text-white">
|
||||
{question.createdAt}
|
||||
</p>
|
||||
</div>
|
||||
@@ -457,11 +448,13 @@
|
||||
<IconButton
|
||||
value="delete-question"
|
||||
onClick={() => {
|
||||
if (!question) return;
|
||||
|
||||
deleteQuestion.shouldDelete = true;
|
||||
deleteQuestion.questionId = question.id;
|
||||
}}
|
||||
>
|
||||
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</header>
|
||||
@@ -485,24 +478,24 @@
|
||||
</div>
|
||||
|
||||
{#each question.comments as comment}
|
||||
<div class="my-5 px-1 flex items-start">
|
||||
<div class="my-5 flex items-start px-1">
|
||||
<Vote
|
||||
value={comment.votes}
|
||||
upVote={() => upvoteQuestion('comment', comment.id)}
|
||||
disabled={voted.comment[comment.id]}
|
||||
/>
|
||||
<div class="w-full rounded-lg border border-1 border-gray">
|
||||
<header class="flex items-center justify-between leading-none p-2">
|
||||
<div class="border-1 border-gray w-full rounded-lg border">
|
||||
<header class="flex items-center justify-between p-2 leading-none">
|
||||
<div class="flex items-center text-black">
|
||||
<Avatar src={comment.avatar} name={comment.name} width="w-7" height="h-7" />
|
||||
<p class="dark:text-white ml-2 text-sm">{comment.name}</p>
|
||||
<p class="dark:text-white ml-2 text-sm text-gray-500">
|
||||
<p class="ml-2 text-sm dark:text-white">{comment.name}</p>
|
||||
<p class="ml-2 text-sm text-gray-500 dark:text-white">
|
||||
{comment.createdAt}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if isValidAnswer}
|
||||
<CheckmarkOutlineIcon size={20} />
|
||||
<CircleCheckIcon size={16} />
|
||||
{/if}
|
||||
|
||||
{#if comment.authorId === $profile.id || $isOrgAdmin}
|
||||
@@ -513,7 +506,7 @@
|
||||
deleteComment.commentId = comment.id;
|
||||
}}
|
||||
>
|
||||
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</header>
|
||||
@@ -528,14 +521,10 @@
|
||||
|
||||
<div>
|
||||
{#if !editorInstance}
|
||||
<TextEditor
|
||||
bind:value={comment}
|
||||
placeholder="Give an answer"
|
||||
onChange={(html) => (comment = html)}
|
||||
/>
|
||||
<TextEditor bind:value={comment} placeholder="Give an answer" onChange={(html) => (comment = html)} />
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-end mr-2">
|
||||
<div class="mr-2 flex justify-end">
|
||||
<PrimaryButton label="Comment" onClick={submitComment} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
import { Dropdown } from 'carbon-components-svelte';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
@@ -14,20 +14,27 @@
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { courses } from '$lib/components/Courses/store';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { fetchCourses } from '$lib/utils/services/courses';
|
||||
|
||||
let errors: {
|
||||
title?: string;
|
||||
courseId?: string;
|
||||
} = {};
|
||||
let fields = {
|
||||
} = $state({});
|
||||
let fields = $state({
|
||||
title: '',
|
||||
body: '',
|
||||
courseId: ''
|
||||
};
|
||||
});
|
||||
|
||||
let fetchedCourses: Course[] = [];
|
||||
let fetchedCourses: Course[] = $state([]);
|
||||
let hasFetched = $state(false);
|
||||
|
||||
async function getCourses(userId: string | null, orgId: string) {
|
||||
async function getCourses(userId?: string, orgId?: string) {
|
||||
if (!userId || !orgId || hasFetched) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasFetched = true;
|
||||
if ($courses.length) {
|
||||
fetchedCourses = [...$courses];
|
||||
return;
|
||||
@@ -70,30 +77,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($profile.id && $currentOrg.id) {
|
||||
getCourses($profile.id, $currentOrg.id);
|
||||
}
|
||||
}
|
||||
$effect(() => {
|
||||
getCourses($profile.id, $currentOrg.id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Ask the Community - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full max-w-3xl mx-auto">
|
||||
<section class="mx-auto w-full max-w-3xl">
|
||||
<div class="p-5">
|
||||
<a class="text-gray-500 dark:text-white text-md flex items-center" href={`/lms/community`}>
|
||||
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" />
|
||||
<a class="text-md flex items-center text-gray-500 dark:text-white" href="/lms/community">
|
||||
<ArrowLeftIcon size={16} />
|
||||
{$t('community.ask.go_back')}
|
||||
</a>
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="dark:text-white text-3xl font-bold">{$t('community.ask.ask_the')}</h1>
|
||||
<h1 class="text-3xl font-bold dark:text-white">{$t('community.ask.ask_the')}</h1>
|
||||
<PrimaryButton label={$t('community.ask.publish')} onClick={handleSave} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 p-2 flex gap-x-5 justify-between">
|
||||
<div class="mb-3 flex justify-between gap-x-5 p-2">
|
||||
<TextField
|
||||
bind:value={fields.title}
|
||||
placeholder={$t('community.ask.title')}
|
||||
@@ -101,7 +106,7 @@
|
||||
className="w-[75%]"
|
||||
/>
|
||||
<Dropdown
|
||||
class="w-[25%] h-full"
|
||||
class="h-full w-[25%]"
|
||||
size="xl"
|
||||
label={$t('community.ask.select_course')}
|
||||
invalid={!!errors.courseId}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import Chip from '$lib/components/Chip/index.svelte';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { browser } from '$app/environment';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { fetchLMSExercises } from '$lib/utils/services/lms/exercises';
|
||||
@@ -37,7 +36,9 @@
|
||||
className: 'text-green-700 bg-green-200'
|
||||
}
|
||||
];
|
||||
let sections: Section[] = cloneDeep(defaultSections);
|
||||
let sections: Section[] = $state(cloneDeep(defaultSections));
|
||||
let hasFetched = $state(false);
|
||||
|
||||
interface Section {
|
||||
id: number;
|
||||
title: string;
|
||||
@@ -75,10 +76,7 @@
|
||||
const lessonURL = `${courseURL}/lessons/${lesson.id}`;
|
||||
const exerciseURL = `${lessonURL}/exercises/${id}`;
|
||||
|
||||
const grade = `${submissionItem.total}/${questions.reduce(
|
||||
(acc, cur) => (acc += cur.points),
|
||||
0
|
||||
)}`;
|
||||
const grade = `${submissionItem.total}/${questions.reduce((acc, cur) => (acc += cur.points), 0)}`;
|
||||
|
||||
const item: ExerciseItem = {
|
||||
exerciseId: id,
|
||||
@@ -100,7 +98,14 @@
|
||||
return _sections;
|
||||
}
|
||||
|
||||
async function fetchData(profileId: string, orgId: string) {
|
||||
async function fetchData(profileId?: string, orgId?: string) {
|
||||
if (hasFetched || !profileId || !orgId) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasFetched = true;
|
||||
|
||||
// Don't rerun this function if any state is updated in this function.
|
||||
const { exercises, error } = await fetchLMSExercises(profileId, orgId);
|
||||
console.log('exercises', exercises);
|
||||
console.log('error', error);
|
||||
@@ -116,51 +121,45 @@
|
||||
console.log('sections', sections);
|
||||
}
|
||||
|
||||
$: if (browser && $profile.id && $currentOrg.id) {
|
||||
$effect(() => {
|
||||
fetchData($profile.id, $currentOrg.id);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="w-full max-w-6xl mx-auto">
|
||||
<section class="mx-auto w-full max-w-6xl">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">{$t('exercises.heading')}</h1>
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-3xl font-bold dark:text-white">{$t('exercises.heading')}</h1>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center w-full">
|
||||
<div class="flex w-full items-center">
|
||||
{#each sections as { title, items, className, id }}
|
||||
<div
|
||||
class="min-w-[355px] max-w-[355px] h-[70vh] rounded-md bg-gray-100 dark:bg-black border border-gray-50 dark:border-neutral-700 p-3 mr-3 overflow-hidden"
|
||||
class="mr-3 h-[70vh] min-w-[355px] max-w-[355px] overflow-hidden rounded-md border border-gray-50 bg-gray-100 p-3 dark:border-neutral-700 dark:bg-black"
|
||||
>
|
||||
<div class="flex items-center mb-2 gap-2">
|
||||
<p class="dark:text-white ml-2 font-bold">{title}</p>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<p class="ml-2 font-bold dark:text-white">{title}</p>
|
||||
<Chip value={items.length} {className} />
|
||||
</div>
|
||||
<div class="pr-2 h-full overflow-y-auto pb-3">
|
||||
<div class="h-full overflow-y-auto pb-3 pr-2">
|
||||
{#each items as item}
|
||||
<div class=" w-full my-2 mx-0 rounded-md bg-white dark:bg-neutral-800 py-3 px-3">
|
||||
<a
|
||||
class="flex w-full items-center cursor-pointer text-primary-600 mb-2"
|
||||
href={item.courseURL}
|
||||
>
|
||||
<div class=" mx-0 my-2 w-full rounded-md bg-white px-3 py-3 dark:bg-neutral-800">
|
||||
<a class="text-primary-600 mb-2 flex w-full cursor-pointer items-center" href={item.courseURL}>
|
||||
<p class="text-xs">{item.courseTitle}</p>
|
||||
</a>
|
||||
<a class="text-black dark:text-white text-md font-bold" href={item.exerciseURL}>
|
||||
<a class="text-md font-bold text-black dark:text-white" href={item.exerciseURL}>
|
||||
{#if id === 3}
|
||||
({item.grade}) -
|
||||
{/if}
|
||||
{item.exerciseTitle}
|
||||
</a>
|
||||
<a
|
||||
class="flex items-center no-underline hover:underline text-black my-2 w-fit"
|
||||
href={item.lessonURL}
|
||||
>
|
||||
<p class="dark:text-white text-grey text-sm">
|
||||
<a class="my-2 flex w-fit items-center text-black no-underline hover:underline" href={item.lessonURL}>
|
||||
<p class="text-grey text-sm dark:text-white">
|
||||
{$t('exercises.lesson')} <span class="italic">{item.lessonTitle}</span>
|
||||
</p>
|
||||
</a>
|
||||
<p class="dark:text-white text-gray-500 text-xs">
|
||||
<p class="text-xs text-gray-500 dark:text-white">
|
||||
{item.submissionUpdatedAt}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { Search, Dropdown } from 'carbon-components-svelte';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { fetchExploreCourses } from '$lib/utils/services/courses';
|
||||
@@ -8,46 +8,45 @@
|
||||
import { courseMetaDeta } from '$lib/components/Courses/store';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { browser } from '$app/environment';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import Grid from 'carbon-icons-svelte/lib/Grid.svelte';
|
||||
import List from 'carbon-icons-svelte/lib/List.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import GridIcon from '@lucide/svelte/icons/grid-2x2';
|
||||
import ListIcon from '@lucide/svelte/icons/list';
|
||||
|
||||
let searchValue = '';
|
||||
let selectedId: string;
|
||||
let filteredExploreCourses: Course[];
|
||||
let searchValue = $state('');
|
||||
let selectedId: string | undefined = $state();
|
||||
let hasFetched = false;
|
||||
let exploreCourseList: Course[] = [];
|
||||
let exploreCourseList: Course[] = $state([]);
|
||||
|
||||
async function getCourses(userId: string | null, orgId: string) {
|
||||
const filteredExploreCourses: Course[] = $derived(filterCourses(searchValue, selectedId || '', exploreCourseList));
|
||||
|
||||
function getCourses(userId?: string, orgId?: string) {
|
||||
if (hasFetched || !userId || !orgId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$courseMetaDeta.isLoading = true;
|
||||
// don't rerun this function if any state is updated in this function.
|
||||
untrack(async () => {
|
||||
$courseMetaDeta.isLoading = true;
|
||||
|
||||
const coursesResult = await fetchExploreCourses(userId, orgId);
|
||||
const coursesResult = await fetchExploreCourses(userId, orgId);
|
||||
|
||||
$courseMetaDeta.isLoading = false;
|
||||
$courseMetaDeta.isLoading = false;
|
||||
|
||||
if (!coursesResult) return;
|
||||
if (!coursesResult) return;
|
||||
|
||||
exploreCourseList = coursesResult.allCourses;
|
||||
hasFetched = true;
|
||||
exploreCourseList = coursesResult.allCourses;
|
||||
hasFetched = true;
|
||||
});
|
||||
}
|
||||
|
||||
function filterCourses(searchValue: string, _selectedId: string, courses: Course[]) {
|
||||
if (browser) {
|
||||
if (!selectedId) {
|
||||
selectedId = localStorage.getItem('classroomio_filter_course_key') || '0';
|
||||
} else {
|
||||
localStorage.setItem('classroomio_filter_course_key', _selectedId);
|
||||
}
|
||||
function filterCourses(searchValue: string, _selectedId: string | null, courses: Course[]) {
|
||||
if (_selectedId) {
|
||||
localStorage.setItem('classroomio_filter_course_key', _selectedId);
|
||||
}
|
||||
|
||||
filteredExploreCourses = courses.filter((course) => {
|
||||
const coursesFiltered = courses.filter((course) => {
|
||||
if (!searchValue || course.title.toLowerCase().includes(searchValue.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
@@ -56,14 +55,12 @@
|
||||
});
|
||||
|
||||
if (_selectedId === '0') {
|
||||
filteredExploreCourses = filteredExploreCourses.sort(
|
||||
(a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
||||
);
|
||||
return coursesFiltered.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
||||
} else if (_selectedId === '1') {
|
||||
filteredExploreCourses = filteredExploreCourses.sort(
|
||||
(a, b) => b.total_lessons - a.total_lessons
|
||||
);
|
||||
return coursesFiltered.sort((a, b) => (b.total_lessons ?? 0) - (a.total_lessons ?? 0));
|
||||
}
|
||||
|
||||
return coursesFiltered;
|
||||
}
|
||||
|
||||
const setViewPreference = (preference: 'grid' | 'list') => {
|
||||
@@ -77,18 +74,18 @@
|
||||
if (courseView) {
|
||||
$courseMetaDeta.view = courseView;
|
||||
}
|
||||
|
||||
selectedId = localStorage.getItem('classroomio_filter_course_key') || '0';
|
||||
});
|
||||
|
||||
$: filterCourses(searchValue, selectedId, exploreCourseList);
|
||||
|
||||
$: if (browser && $profile.id && $currentOrg.id) {
|
||||
$effect(() => {
|
||||
getCourses($profile.id, $currentOrg.id);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="w-full md:max-w-6xl md:mx-auto">
|
||||
<div class="py-2 md:py-10 px-2 md:px-5">
|
||||
<h1 class="dark:text-white text-3xl font-bold">{$t('explore.heading')}</h1>
|
||||
<section class="w-full md:mx-auto md:max-w-6xl">
|
||||
<div class="px-2 py-2 md:px-5 md:py-10">
|
||||
<h1 class="text-3xl font-bold dark:text-white">{$t('explore.heading')}</h1>
|
||||
<div class="flex flex-row-reverse">
|
||||
<div class="filter-container flex items-end justify-start">
|
||||
<Search
|
||||
@@ -109,17 +106,17 @@
|
||||
|
||||
{#if $courseMetaDeta.view === 'list'}
|
||||
<IconButton onClick={() => setViewPreference('grid')}>
|
||||
<Grid size={24} />
|
||||
<GridIcon size={16} />
|
||||
</IconButton>
|
||||
{:else}
|
||||
<IconButton onClick={() => setViewPreference('list')}>
|
||||
<List size={24} />
|
||||
<ListIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<Courses
|
||||
bind:courses={filteredExploreCourses}
|
||||
courses={filteredExploreCourses}
|
||||
emptyTitle={$t('explore.empty_heading')}
|
||||
emptyDescription={$t('explore.empty_description')}
|
||||
isExplore={true}
|
||||
|
||||
@@ -6,13 +6,8 @@
|
||||
import { fetchCourses } from '$lib/utils/services/courses';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import {
|
||||
courses,
|
||||
courseMetaDeta,
|
||||
coursesComplete,
|
||||
coursesInProgress
|
||||
} from '$lib/components/Courses/store';
|
||||
import { browser } from '$app/environment';
|
||||
import { courses, courseMetaDeta, coursesComplete, coursesInProgress } from '$lib/components/Courses/store';
|
||||
import { untrack } from 'svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
let hasFetched = false;
|
||||
@@ -21,32 +16,36 @@
|
||||
return () => (currentTab = tab);
|
||||
}
|
||||
|
||||
async function getCourses(userId: string | null, orgId: string) {
|
||||
function getCourses(userId?: string, orgId?: string) {
|
||||
if (hasFetched || !userId || !orgId) {
|
||||
return;
|
||||
}
|
||||
hasFetched = true;
|
||||
|
||||
// only show is loading when fetching for the first time
|
||||
if (!$courses.length) {
|
||||
$courseMetaDeta.isLoading = true;
|
||||
}
|
||||
// don't rerun this function if any state is updated in this function.
|
||||
untrack(async () => {
|
||||
hasFetched = true;
|
||||
|
||||
const coursesResult = await fetchCourses(userId, orgId);
|
||||
console.log(`get courses result`, coursesResult);
|
||||
// only show is loading when fetching for the first time
|
||||
if (!$courses.length) {
|
||||
$courseMetaDeta.isLoading = true;
|
||||
}
|
||||
|
||||
$courseMetaDeta.isLoading = false;
|
||||
if (!coursesResult) return;
|
||||
const coursesResult = await fetchCourses(userId, orgId);
|
||||
console.log(`get courses result`, coursesResult);
|
||||
|
||||
courses.set(coursesResult.allCourses);
|
||||
hasFetched = true;
|
||||
$courseMetaDeta.isLoading = false;
|
||||
if (!coursesResult) return;
|
||||
|
||||
courses.set(coursesResult.allCourses);
|
||||
hasFetched = true;
|
||||
});
|
||||
}
|
||||
|
||||
$: if (browser && $profile.id && $currentOrg.id) {
|
||||
$effect(() => {
|
||||
getCourses($profile.id, $currentOrg.id);
|
||||
}
|
||||
});
|
||||
|
||||
$: tabs = [
|
||||
let tabs = $derived([
|
||||
{
|
||||
label: `${$t('my_learning.progress')} (${$coursesInProgress.length})`,
|
||||
value: '1'
|
||||
@@ -55,33 +54,35 @@
|
||||
label: `${$t('my_learning.complete')} (${$coursesComplete.length})`,
|
||||
value: '2'
|
||||
}
|
||||
];
|
||||
$: currentTab = tabs[0].value;
|
||||
]);
|
||||
let currentTab = $derived(tabs[0].value);
|
||||
</script>
|
||||
|
||||
<section class="max-w-6xl mx-auto">
|
||||
<section class="mx-auto max-w-6xl">
|
||||
<div class="m-2 md:m-5">
|
||||
<div role="searchbox" class=" bg-gray-100 w-full md:w-[60%] lg:w-[30%]">
|
||||
<div role="searchbox" class=" w-full bg-gray-100 md:w-[60%] lg:w-[30%]">
|
||||
<Search placeholder={$t('my_learning.search')} class="dark:text-black" />
|
||||
</div>
|
||||
<h1 class="text-3xl font-semibold my-4">{$t('my_learning.heading')}</h1>
|
||||
<h1 class="my-4 text-3xl font-semibold">{$t('my_learning.heading')}</h1>
|
||||
<Tabs {tabs} {currentTab} {onChange}>
|
||||
<slot:fragment slot="content">
|
||||
<TabContent value={tabs[0].value} index={currentTab}>
|
||||
<Courses
|
||||
courses={$coursesInProgress}
|
||||
emptyTitle={$t('my_learning.not_in_progress')}
|
||||
emptyDescription={$t('my_learning.any_progress')}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={tabs[1].value} index={currentTab}>
|
||||
<Courses
|
||||
courses={$coursesComplete}
|
||||
emptyTitle={$t('my_learning.not_completed')}
|
||||
emptyDescription={$t('my_learning.any_course')}
|
||||
/>
|
||||
</TabContent>
|
||||
</slot:fragment>
|
||||
{#snippet content()}
|
||||
<slot:fragment>
|
||||
<TabContent value={tabs[0].value} index={currentTab}>
|
||||
<Courses
|
||||
courses={$coursesInProgress}
|
||||
emptyTitle={$t('my_learning.not_in_progress')}
|
||||
emptyDescription={$t('my_learning.any_progress')}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={tabs[1].value} index={currentTab}>
|
||||
<Courses
|
||||
courses={$coursesComplete}
|
||||
emptyTitle={$t('my_learning.not_completed')}
|
||||
emptyDescription={$t('my_learning.any_course')}
|
||||
/>
|
||||
</TabContent>
|
||||
</slot:fragment>
|
||||
{/snippet}
|
||||
</Tabs>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { Tabs, Tab, TabContent } from 'carbon-components-svelte';
|
||||
import Profile from '$lib/components/LMS/components/Profile.svelte';
|
||||
import Integrations from '$lib/components/Org/Settings/Integrations.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
let selected = 0;
|
||||
let tabs = [];
|
||||
$: tabs = [
|
||||
let selected = $state(0);
|
||||
const tabs = $derived([
|
||||
{
|
||||
key: 0,
|
||||
label: $t('settings.tabs.profile_tab'),
|
||||
tabKey: '',
|
||||
href: $page.url.pathname
|
||||
href: page.url.pathname
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
label: $t('settings.tabs.integrations_tab'),
|
||||
tabKey: 'integrations',
|
||||
href: `${$page.url.pathname}?tab=integrations`,
|
||||
href: `${page.url.pathname}?tab=integrations`,
|
||||
disabled: false
|
||||
}
|
||||
];
|
||||
]);
|
||||
</script>
|
||||
|
||||
<section class="w-full max-w-6xl mx-auto">
|
||||
<div class="py-10 px-5">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">{$t('settings.heading')}</h1>
|
||||
<section class="mx-auto w-full max-w-6xl">
|
||||
<div class="px-5 py-10">
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-3xl font-bold dark:text-white">{$t('settings.heading')}</h1>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
|
||||
let formRef: HTMLFormElement;
|
||||
let formRef: HTMLFormElement = $state();
|
||||
let supabase = getSupabase();
|
||||
let fields = Object.assign({}, LOGIN_FIELDS);
|
||||
let submitError: string;
|
||||
let loading = false;
|
||||
let errors = Object.assign({}, LOGIN_FIELDS);
|
||||
let fields = $state(Object.assign({}, LOGIN_FIELDS));
|
||||
let submitError: string = $state();
|
||||
let loading = $state(false);
|
||||
let errors = $state(Object.assign({}, LOGIN_FIELDS));
|
||||
|
||||
async function handleSubmit() {
|
||||
const validationRes = authValidation(fields);
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="app-background w-full min-h-screen flex items-center justify-center">
|
||||
<div class="app-background flex min-h-screen w-full items-center justify-center">
|
||||
<div>Logging you out...</div>
|
||||
</div>
|
||||
|
||||
@@ -12,12 +12,10 @@
|
||||
import { blockedSubdomain } from '$lib/utils/constants/app';
|
||||
import { getOrganizations } from '$lib/utils/services/org';
|
||||
import { generateSitename } from '$lib/utils/functions/org';
|
||||
import {
|
||||
triggerSendEmail,
|
||||
NOTIFICATION_NAME
|
||||
} from '$lib/utils/services/notification/notification';
|
||||
import { triggerSendEmail, NOTIFICATION_NAME } from '$lib/utils/services/notification/notification';
|
||||
import { handleLocaleChange, t } from '$lib/utils/functions/translations';
|
||||
import { LOCALE } from '$lib/utils/types';
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
interface OnboardingField {
|
||||
fullname?: string;
|
||||
@@ -35,17 +33,16 @@
|
||||
}
|
||||
|
||||
const maxSteps = 2;
|
||||
let fields: OnboardingField = {
|
||||
let fields: OnboardingField = $state({
|
||||
fullname: '',
|
||||
orgName: '',
|
||||
siteName: '',
|
||||
locale: LOCALE.EN
|
||||
};
|
||||
let errors: OnboardingField = {};
|
||||
let progress = 50;
|
||||
let step = 1;
|
||||
let loading = false;
|
||||
let isSiteNameTouched = false;
|
||||
});
|
||||
let errors: OnboardingField = $state({});
|
||||
let step = $state(1);
|
||||
let loading = $state(false);
|
||||
let isSiteNameTouched = $state(false);
|
||||
let selectedId = 'en';
|
||||
|
||||
let dropdownItems = [
|
||||
@@ -61,6 +58,8 @@
|
||||
{ id: 'da', text: 'Danish' }
|
||||
];
|
||||
|
||||
const progress = $derived(Math.round((step / maxSteps) * 100));
|
||||
|
||||
const educatorGoals: Goal[] = [
|
||||
{
|
||||
label: $t('onboarding.sell'),
|
||||
@@ -116,15 +115,6 @@
|
||||
fields.metadata = payload;
|
||||
}
|
||||
|
||||
function setOrgSiteName(orgName: string | undefined, isTouched: boolean) {
|
||||
if (!orgName || isTouched) return;
|
||||
|
||||
fields.siteName = orgName
|
||||
?.toLowerCase()
|
||||
?.replace(/\s+/g, '-')
|
||||
?.replace(/[^a-zA-Z0-9-]/g, '');
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
loading = true;
|
||||
// Run validation
|
||||
@@ -233,9 +223,30 @@
|
||||
loading = false;
|
||||
};
|
||||
|
||||
$: progress = Math.round((step / maxSteps) * 100);
|
||||
$: fields.siteName = generateSitename(fields.siteName || '');
|
||||
$: setOrgSiteName(fields.orgName, isSiteNameTouched);
|
||||
function updateSiteName(sname?: string) {
|
||||
if (!sname) return;
|
||||
|
||||
untrack(() => {
|
||||
fields.siteName = generateSitename(sname);
|
||||
});
|
||||
}
|
||||
$effect(() => {
|
||||
updateSiteName(fields.siteName);
|
||||
});
|
||||
|
||||
function setOrgSiteName(orgName: string | undefined, isTouched: boolean) {
|
||||
if (!orgName || isTouched) return;
|
||||
|
||||
untrack(() => {
|
||||
fields.siteName = orgName
|
||||
?.toLowerCase()
|
||||
?.replace(/\s+/g, '-')
|
||||
?.replace(/[^a-zA-Z0-9-]/g, '');
|
||||
});
|
||||
}
|
||||
$effect(() => {
|
||||
setOrgSiteName(fields.orgName, isSiteNameTouched);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $profile.id}
|
||||
@@ -252,7 +263,7 @@
|
||||
<div
|
||||
class="mb-6 flex w-64 items-center justify-center rounded-2xl border border-gray-300 bg-gray-100 py-6 dark:bg-neutral-800"
|
||||
>
|
||||
<UserProfileIcon />
|
||||
<UserProfileIcon size={16} />
|
||||
<p class="ml-2 text-sm dark:text-white">{$profile.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,13 +328,7 @@
|
||||
<!-- Loop through Goals -->
|
||||
{#each educatorGoals as goal}
|
||||
<label class="mb-1 inline-flex w-full items-center font-light dark:text-white">
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={fields.goal}
|
||||
name="goal"
|
||||
value={goal.value}
|
||||
class="mr-2"
|
||||
/>
|
||||
<input type="radio" bind:group={fields.goal} name="goal" value={goal.value} class="mr-2" />
|
||||
{goal.label}
|
||||
</label>
|
||||
{/each}
|
||||
@@ -344,13 +349,7 @@
|
||||
<!-- Loop through Goals -->
|
||||
{#each sources as source}
|
||||
<label class="mb-1 inline-flex w-full items-center font-light dark:text-white">
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={fields.source}
|
||||
name="source"
|
||||
value={source.value}
|
||||
class="mr-2"
|
||||
/>
|
||||
<input type="radio" bind:group={fields.source} name="source" value={source.value} class="mr-2" />
|
||||
{source.label}
|
||||
</label>
|
||||
{/each}
|
||||
@@ -365,12 +364,7 @@
|
||||
<!-- Language Picker -->
|
||||
<div class="mt-10">
|
||||
<span>{$t('content.toggle_label')}: </span>
|
||||
<Dropdown
|
||||
items={dropdownItems}
|
||||
{selectedId}
|
||||
on:select={handleSelect}
|
||||
class="w-full"
|
||||
/>
|
||||
<Dropdown items={dropdownItems} {selectedId} on:select={handleSelect} class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -380,10 +374,7 @@
|
||||
<!-- Footer -->
|
||||
<div class="mt-8 flex w-full items-center justify-between">
|
||||
<div class="relative h-2 w-24 bg-gray-300">
|
||||
<span
|
||||
class="progress bg-primary-700 absolute left-0 top-0 h-full"
|
||||
style="width: {progress}%;"
|
||||
/>
|
||||
<span class="progress bg-primary-700 absolute left-0 top-0 h-full" style="width: {progress}%;"></span>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import AddOrgModal from '$lib/components/Org/AddOrgModal/AddOrgModal.svelte';
|
||||
import { isQuizPage } from '$lib/utils/functions/app';
|
||||
import OrgSideBar from '$lib/components/Org/SideBar.svelte';
|
||||
@@ -8,29 +8,31 @@
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let data;
|
||||
let { data, children } = $props();
|
||||
|
||||
let ref = null;
|
||||
|
||||
$: if ($currentOrg.id && data.orgName === '*') {
|
||||
const newUrl = $page.url.pathname.replace('*', $currentOrg.siteName);
|
||||
goto(newUrl + $page.url.search);
|
||||
function redirect(siteName: string) {
|
||||
const newUrl = page.url.pathname.replace('*', siteName);
|
||||
goto(newUrl + page.url.search);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
data.orgName === '*' && redirect($currentOrg.siteName);
|
||||
});
|
||||
</script>
|
||||
|
||||
<AddOrgModal />
|
||||
|
||||
<VerifyEmailModal />
|
||||
|
||||
<div class="org-root w-full flex items-center justify-between">
|
||||
{#if !isQuizPage($page.url?.pathname)}
|
||||
<div class="org-root flex w-full items-center justify-between">
|
||||
{#if !isQuizPage(page.url?.pathname)}
|
||||
<OrgSideBar />
|
||||
{/if}
|
||||
<div class="org-slot bg-white dark:bg-black w-full">
|
||||
<div class="org-slot w-full bg-white dark:bg-black">
|
||||
{#if data.orgName === '*'}
|
||||
<Box>Taking you to your organization...</Box>
|
||||
{:else}
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { ActivityCard } from '$lib/components/Analytics';
|
||||
import CourseIcon from '$lib/components/Icons/CourseIcon.svelte';
|
||||
import Progress from '$lib/components/Progress/index.svelte';
|
||||
import UserProfile from 'carbon-icons-svelte/lib/UserProfile.svelte';
|
||||
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
|
||||
import UserIcon from '@lucide/svelte/icons/user';
|
||||
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
@@ -11,10 +11,11 @@
|
||||
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import type { OrganisationAnalytics } from '$lib/utils/types/analytics';
|
||||
import Add from 'carbon-icons-svelte/lib/Add.svelte';
|
||||
import Book from 'carbon-icons-svelte/lib/Book.svelte';
|
||||
import CurrencyDollar from 'carbon-icons-svelte/lib/CurrencyDollar.svelte';
|
||||
import UserMultiple from 'carbon-icons-svelte/lib/UserMultiple.svelte';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
|
||||
import BookIcon from '@lucide/svelte/icons/book';
|
||||
import DollarSignIcon from '@lucide/svelte/icons/dollar-sign';
|
||||
import UsersIcon from '@lucide/svelte/icons/users';
|
||||
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
|
||||
@@ -27,16 +28,17 @@
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { Grid, Link, SkeletonPlaceholder } from 'carbon-components-svelte';
|
||||
|
||||
// export let data;
|
||||
|
||||
let dashAnalytics: OrganisationAnalytics;
|
||||
let dashAnalytics: OrganisationAnalytics | undefined = $state();
|
||||
let hasFetched = $state(false);
|
||||
|
||||
function createCourse() {
|
||||
goto(`${$currentOrgPath}/courses?create=true`);
|
||||
}
|
||||
|
||||
async function fetchDashAnalytics(orgId: string) {
|
||||
if (!orgId) return;
|
||||
if (!orgId || hasFetched) return;
|
||||
|
||||
hasFetched = true;
|
||||
|
||||
const accessToken = await getAccessToken();
|
||||
|
||||
@@ -61,31 +63,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: fetchDashAnalytics($currentOrg.id);
|
||||
$effect(() => {
|
||||
fetchDashAnalytics($currentOrg.id);
|
||||
});
|
||||
|
||||
$: cards = [
|
||||
let cards = $derived([
|
||||
{
|
||||
icon: CurrencyDollar,
|
||||
icon: DollarSignIcon,
|
||||
title: `${$t('dashboard.revenue')} ($)`,
|
||||
percentage: dashAnalytics?.revenue ?? 0,
|
||||
description: $t('dashboard.revenue_description'),
|
||||
hidePercentage: true
|
||||
},
|
||||
{
|
||||
icon: Book,
|
||||
icon: BookIcon,
|
||||
title: $t('dashboard.no_of_courses'),
|
||||
percentage: dashAnalytics?.numberOfCourses ?? 0,
|
||||
description: $t('dashboard.no_courses_description'),
|
||||
hidePercentage: true
|
||||
},
|
||||
{
|
||||
icon: UserMultiple,
|
||||
icon: UsersIcon,
|
||||
title: $t('dashboard.total_students'),
|
||||
percentage: dashAnalytics?.totalStudents ?? 0,
|
||||
description: $t('dashboard.total_students_description'),
|
||||
hidePercentage: true
|
||||
}
|
||||
];
|
||||
]);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -96,7 +100,7 @@
|
||||
|
||||
<div class="w-full max-w-5xl px-5 py-10 md:mx-auto">
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<h1 class="mb-3 text-2xl font-bold dark:text-white md:text-3xl">
|
||||
<h1 class="mb-3 text-2xl dark:text-white">
|
||||
{$t(getGreeting())}
|
||||
{$profile.fullname}!
|
||||
</h1>
|
||||
@@ -108,7 +112,7 @@
|
||||
className="min-h-[36px]"
|
||||
>
|
||||
{#if $isMobile}
|
||||
<Add size={24} />
|
||||
<PlusIcon size={16} />
|
||||
{:else}
|
||||
{$t('dashboard.create_course')}
|
||||
{/if}
|
||||
@@ -123,10 +127,7 @@
|
||||
<div class="mt-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each cards as card}
|
||||
{#if !dashAnalytics}
|
||||
<SkeletonPlaceholder
|
||||
style="width: 100%; min-width: 300px; height: 10rem;"
|
||||
class="rounded-md"
|
||||
/>
|
||||
<SkeletonPlaceholder style="width: 100%; min-width: 300px; height: 10rem;" class="rounded-md" />
|
||||
{:else}
|
||||
<ActivityCard activity={card} />
|
||||
{/if}
|
||||
@@ -136,9 +137,7 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div
|
||||
class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 dark:border-neutral-600 md:p-5"
|
||||
>
|
||||
<div class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 md:p-5 dark:border-neutral-600">
|
||||
<h3 class="mt-0 text-2xl font-bold">
|
||||
{$t('dashboard.top_courses')}
|
||||
</h3>
|
||||
@@ -173,7 +172,7 @@
|
||||
{:else}
|
||||
<div class="flex flex-col h-full items-center justify-center p-3">
|
||||
<div class="bg-primary-200 w-fit rounded-full p-4 text-black">
|
||||
<CourseIcon width="30" height="30" />
|
||||
<LibraryBigIcon size={30} />
|
||||
</div>
|
||||
<div class="my-4 text-center">
|
||||
<p class=" text-xl font-semibold">
|
||||
@@ -183,20 +182,14 @@
|
||||
{$t('dashboard.create_first_course_description')}
|
||||
</p>
|
||||
</div>
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={createCourse}
|
||||
label={$t('dashboard.create_course')}
|
||||
/>
|
||||
<PrimaryButton variant={VARIANTS.OUTLINED} onClick={createCourse} label={$t('dashboard.create_course')} />
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 dark:border-neutral-600 md:p-5"
|
||||
>
|
||||
<div class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 md:p-5 dark:border-neutral-600">
|
||||
<h3 class="mt-0 text-2xl font-bold">
|
||||
{$t('dashboard.recent_enrollments')}
|
||||
</h3>
|
||||
@@ -210,12 +203,7 @@
|
||||
{#each dashAnalytics.enrollments as enrollment}
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Avatar
|
||||
src={enrollment.avatarUrl}
|
||||
name={enrollment.name}
|
||||
width="w-6"
|
||||
height="h-6"
|
||||
/>
|
||||
<Avatar src={enrollment.avatarUrl} name={enrollment.name} width="w-6" height="h-6" />
|
||||
|
||||
<div class="min-h-[45px] space-y-1">
|
||||
<p class="text-sm font-medium capitalize leading-none">{enrollment.name}</p>
|
||||
@@ -238,7 +226,7 @@
|
||||
{:else}
|
||||
<div class="flex flex-col h-full items-center justify-center p-3">
|
||||
<div class="bg-primary-200 w-fit rounded-full p-4 text-black">
|
||||
<UserProfile size={24} />
|
||||
<UserIcon size={16} />
|
||||
</div>
|
||||
<div class="my-4 text-center">
|
||||
<p class=" text-xl font-semibold">
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import Audience from '$lib/components/Org/Audience/index.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { orgAudience, currentOrgPlan, currentOrgMaxAudience } from '$lib/utils/store/org';
|
||||
import { PLAN } from 'shared/src/plans/constants';
|
||||
import { PLAN } from '@cio/utils/plans';
|
||||
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||
|
||||
let isLoading = false;
|
||||
let isLoading = $state(false);
|
||||
|
||||
function exportAudience() {
|
||||
isLoading = true;
|
||||
@@ -19,23 +19,18 @@
|
||||
<title>Audience - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full max-w-4xl mx-auto">
|
||||
<div class="py-10 px-5">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<section class="mx-auto w-full max-w-4xl">
|
||||
<div class="px-5 py-10">
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<div class="flex items-end">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold m-0">{$t('audience.title')}</h1>
|
||||
<h1 class="m-0 text-2xl font-bold md:text-3xl dark:text-white">{$t('audience.title')}</h1>
|
||||
{#if $currentOrgPlan?.plan_name !== PLAN.ENTERPRISE}
|
||||
<span class="ml-2">
|
||||
({$orgAudience.length} / {$currentOrgMaxAudience})
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<PrimaryButton
|
||||
label={$t('audience.export')}
|
||||
onClick={exportAudience}
|
||||
isDisabled={isLoading}
|
||||
{isLoading}
|
||||
/>
|
||||
<PrimaryButton label={$t('audience.export')} onClick={exportAudience} isDisabled={isLoading} {isLoading} />
|
||||
</div>
|
||||
|
||||
{#if $orgAudience.length >= $currentOrgMaxAudience}
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
import { currentOrgPath } from '$lib/utils/store/org';
|
||||
import type { UserAnalytics } from '$lib/utils/types/analytics';
|
||||
import { Grid, Tag } from 'carbon-components-svelte';
|
||||
import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
|
||||
import Notebook from 'carbon-icons-svelte/lib/Notebook.svelte';
|
||||
import Report from 'carbon-icons-svelte/lib/Report.svelte';
|
||||
import RowExpand from 'carbon-icons-svelte/lib/RowExpand.svelte';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
import BookOpenIcon from '@lucide/svelte/icons/book-open';
|
||||
import ChartLineIcon from '@lucide/svelte/icons/chart-line';
|
||||
import UnfoldVerticalIcon from '@lucide/svelte/icons/unfold-vertical';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
export let data;
|
||||
let { data } = $props();
|
||||
|
||||
let userAnalytics: UserAnalytics;
|
||||
let userAnalytics: UserAnalytics | undefined = $state();
|
||||
|
||||
let courseFilter = 'all';
|
||||
let courseFilter = $state('all');
|
||||
function toggleCourseFilter(filter: 'completed' | 'incomplete') {
|
||||
if (courseFilter === filter) {
|
||||
courseFilter = 'all';
|
||||
@@ -50,51 +50,50 @@
|
||||
fetchUserAnalytics();
|
||||
});
|
||||
|
||||
$: userMetrics = [
|
||||
let userMetrics = $derived([
|
||||
{
|
||||
icon: Notebook,
|
||||
icon: BookOpenIcon,
|
||||
title: $t('analytics.enrolled_courses'),
|
||||
description: $t('analytics.enrolled_courses_description'),
|
||||
percentage: userAnalytics?.courses?.length,
|
||||
hidePercentage: true
|
||||
},
|
||||
{
|
||||
icon: Report,
|
||||
icon: ChartLineIcon,
|
||||
title: $t('analytics.overall_course_progress'),
|
||||
description: $t('analytics.overall_course_progress_description'),
|
||||
percentage: userAnalytics?.overallCourseProgress
|
||||
},
|
||||
{
|
||||
icon: RowExpand,
|
||||
icon: UnfoldVerticalIcon,
|
||||
title: $t('analytics.total_average_grade'),
|
||||
description: $t('analytics.total_average_grade_description'),
|
||||
percentage: userAnalytics?.overallAverageGrade
|
||||
}
|
||||
];
|
||||
]);
|
||||
|
||||
$: completedCourses = userAnalytics?.courses?.filter(
|
||||
(course) => course.lessons_count === course.lessons_completed
|
||||
)?.length;
|
||||
$: incompleteCourses = userAnalytics?.courses?.filter(
|
||||
(course) => course.lessons_count !== course.lessons_completed
|
||||
)?.length;
|
||||
let completedCourses = $derived(
|
||||
userAnalytics?.courses?.filter((course) => course.lessons_count === course.lessons_completed)?.length
|
||||
);
|
||||
let incompleteCourses = $derived(
|
||||
userAnalytics?.courses?.filter((course) => course.lessons_count !== course.lessons_completed)?.length
|
||||
);
|
||||
|
||||
$: filteredCourses = userAnalytics?.courses?.filter((course) => {
|
||||
if (courseFilter === 'all') {
|
||||
return true;
|
||||
}
|
||||
return (course.lessons_count === course.lessons_completed) === (courseFilter === 'completed');
|
||||
});
|
||||
let filteredCourses = $derived(
|
||||
userAnalytics?.courses?.filter((course) => {
|
||||
if (courseFilter === 'all') {
|
||||
return true;
|
||||
}
|
||||
return (course.lessons_count === course.lessons_completed) === (courseFilter === 'completed');
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if userAnalytics}
|
||||
<section class="w-full md:mx-auto md:max-w-5xl">
|
||||
<div class="p-5">
|
||||
<a
|
||||
class="text-md flex items-center text-gray-500 dark:text-white"
|
||||
href={`${$currentOrgPath}/audience`}
|
||||
>
|
||||
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" />
|
||||
<a class="text-md flex items-center text-gray-500 dark:text-white" href={`${$currentOrgPath}/audience`}>
|
||||
<ArrowLeftIcon size={16} />
|
||||
{$t('community.ask.go_back')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { currentOrgPath } from '$lib/utils/store/org';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import Add from 'carbon-icons-svelte/lib/Add.svelte';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
function askCommunity() {
|
||||
@@ -17,20 +17,16 @@
|
||||
<title>Community - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full md:max-w-4xl md:mx-auto">
|
||||
<div class="w-full py-10 px-5">
|
||||
<div class="flex items-center justify-between mb-10 w-full">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">{$t('community.title')}</h1>
|
||||
<section class="w-full md:mx-auto md:max-w-4xl">
|
||||
<div class="w-full px-5 py-10">
|
||||
<div class="mb-10 flex w-full items-center justify-between">
|
||||
<h1 class="text-2xl font-bold md:text-3xl dark:text-white">{$t('community.title')}</h1>
|
||||
{#if $isMobile}
|
||||
<PrimaryButton onClick={askCommunity}>
|
||||
<Add size={24} />
|
||||
<PlusIcon size={16} />
|
||||
</PrimaryButton>
|
||||
{:else}
|
||||
<PrimaryButton
|
||||
label={$t('community.ask_button')}
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
onClick={askCommunity}
|
||||
/>
|
||||
<PrimaryButton label={$t('community.ask_button')} variant={VARIANTS.CONTAINED_DARK} onClick={askCommunity} />
|
||||
{/if}
|
||||
</div>
|
||||
<Community />
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
import { untrack } from 'svelte';
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import { courses } from '$lib/components/Courses/store';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import DeleteModal from '$lib/components/Org/Community/DeleteModal.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
@@ -14,21 +14,18 @@
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import {
|
||||
askCommunityValidation,
|
||||
commentInCommunityValidation
|
||||
} from '$lib/utils/functions/validator';
|
||||
import { askCommunityValidation, commentInCommunityValidation } from '$lib/utils/functions/validator';
|
||||
import { fetchCourses } from '$lib/utils/services/courses';
|
||||
import { currentOrg, currentOrgPath, isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { Dropdown, SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
|
||||
import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
|
||||
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
|
||||
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import pluralize from 'pluralize';
|
||||
|
||||
export let data;
|
||||
let { data } = $props();
|
||||
const { slug } = data;
|
||||
|
||||
interface Comment {
|
||||
@@ -56,11 +53,11 @@
|
||||
courseId: string;
|
||||
}
|
||||
|
||||
let question: Question;
|
||||
let comment = '';
|
||||
let question: Question | undefined = $state();
|
||||
let comment = $state('');
|
||||
let errors: {
|
||||
title?: string;
|
||||
} = {};
|
||||
} = $state({});
|
||||
let isValidAnswer = false; // V2 allow admin mark an answer as accepted
|
||||
let resetInput = 1;
|
||||
let voted: {
|
||||
@@ -68,26 +65,26 @@
|
||||
comment: {
|
||||
[key: string]: boolean;
|
||||
};
|
||||
} = { question: false, comment: {} };
|
||||
let isEditMode = false;
|
||||
let deleteComment = {
|
||||
} = $state({ question: false, comment: {} });
|
||||
let isEditMode = $state(false);
|
||||
let deleteComment = $state({
|
||||
shouldDelete: false,
|
||||
commentId: '',
|
||||
isDeleting: false
|
||||
};
|
||||
let deleteQuestion = {
|
||||
});
|
||||
let deleteQuestion = $state({
|
||||
shouldDelete: false,
|
||||
questionId: '',
|
||||
isDeleting: false
|
||||
};
|
||||
let editContent = {
|
||||
});
|
||||
let editContent = $state({
|
||||
title: '',
|
||||
body: '',
|
||||
courseId: ''
|
||||
};
|
||||
});
|
||||
|
||||
let editorInstance = false;
|
||||
let fetchedCourses: Course[] = [];
|
||||
let editorInstance = $state(false);
|
||||
let fetchedCourses: Course[] = $state([]);
|
||||
|
||||
function mapResToQuestion(data): Question {
|
||||
return {
|
||||
@@ -115,14 +112,22 @@
|
||||
};
|
||||
}
|
||||
|
||||
async function getCourses(userId: string | null, orgId: string) {
|
||||
if ($courses.length) {
|
||||
fetchedCourses = [...$courses];
|
||||
return;
|
||||
}
|
||||
let hasFetched = $state(false);
|
||||
function getCourses(userId: string | null, orgId: string) {
|
||||
if (hasFetched) return;
|
||||
|
||||
const coursesResults = await fetchCourses(userId, orgId);
|
||||
fetchedCourses = coursesResults?.allCourses || [];
|
||||
untrack(async () => {
|
||||
if ($courses.length) {
|
||||
fetchedCourses = [...$courses];
|
||||
hasFetched = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const coursesResults = await fetchCourses(userId, orgId);
|
||||
fetchedCourses = coursesResults?.allCourses || [];
|
||||
|
||||
hasFetched = true;
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchCommunityQuestion(slug: string) {
|
||||
@@ -157,18 +162,20 @@
|
||||
|
||||
if (error) {
|
||||
console.error('[ORG] Error loading community', error);
|
||||
return goto(`${currentOrgPath}`);
|
||||
return goto(`${$currentOrgPath}`);
|
||||
}
|
||||
|
||||
question = mapResToQuestion(data);
|
||||
question.totalComments = question.comments.length;
|
||||
untrack(() => {
|
||||
question = mapResToQuestion(data);
|
||||
question.totalComments = question.comments.length;
|
||||
});
|
||||
}
|
||||
|
||||
async function submitComment() {
|
||||
errors = commentInCommunityValidation({ comment });
|
||||
console.log('submitComment errors', errors);
|
||||
|
||||
if (Object.keys(errors).length) {
|
||||
if (Object.keys(errors).length || !question) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -213,6 +220,8 @@
|
||||
}
|
||||
|
||||
async function upvoteQuestion(type: string, commentId?: string) {
|
||||
if (!question) return;
|
||||
|
||||
const isQuestion = type === 'question';
|
||||
|
||||
if (isQuestion && voted.question) return;
|
||||
@@ -248,6 +257,8 @@
|
||||
}
|
||||
|
||||
async function handleQuestionEdit() {
|
||||
if (!question) return;
|
||||
|
||||
if (isEditMode) {
|
||||
errors = askCommunityValidation(editContent);
|
||||
console.log('handleQuestionEdit errors', errors);
|
||||
@@ -295,13 +306,12 @@
|
||||
}
|
||||
|
||||
async function handleDelete(isQuestion: boolean) {
|
||||
if (!question) return;
|
||||
|
||||
if (!isQuestion) {
|
||||
deleteComment.isDeleting = true;
|
||||
|
||||
const { error } = await supabase
|
||||
.from('community_answer')
|
||||
.delete()
|
||||
.match({ id: deleteComment.commentId });
|
||||
const { error } = await supabase.from('community_answer').delete().match({ id: deleteComment.commentId });
|
||||
|
||||
deleteComment.isDeleting = false;
|
||||
|
||||
@@ -350,12 +360,14 @@
|
||||
deleteQuestion.isDeleting = false;
|
||||
}
|
||||
|
||||
$: browser && fetchCommunityQuestion(slug);
|
||||
$: {
|
||||
$effect(() => {
|
||||
fetchCommunityQuestion(slug);
|
||||
});
|
||||
$effect(() => {
|
||||
if ($profile.id && $currentOrg.id) {
|
||||
getCourses($profile.id, $currentOrg.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -364,7 +376,7 @@
|
||||
|
||||
<DeleteModal
|
||||
bind:open={deleteQuestion.shouldDelete}
|
||||
bind:isDeleting={deleteQuestion.isDeleting}
|
||||
isDeleting={deleteQuestion.isDeleting}
|
||||
onCancel={() => {
|
||||
deleteQuestion.shouldDelete = false;
|
||||
deleteQuestion.questionId = '';
|
||||
@@ -391,20 +403,13 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="px-5 py-10">
|
||||
<a
|
||||
class="text-md flex items-center text-gray-500 dark:text-white"
|
||||
href={`${$currentOrgPath}/community`}
|
||||
>
|
||||
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" />
|
||||
<a class="text-md flex items-center text-gray-500 dark:text-white" href={`${$currentOrgPath}/community`}>
|
||||
<ArrowLeftIcon size={16} />
|
||||
{$t('community.ask.go_back')}
|
||||
</a>
|
||||
<div class="my-5 flex items-center justify-between">
|
||||
{#if isEditMode}
|
||||
<TextField
|
||||
bind:value={editContent.title}
|
||||
className="w-full mr-2"
|
||||
errorMessage={errors.title}
|
||||
/>
|
||||
<TextField bind:value={editContent.title} className="w-full mr-2" errorMessage={errors.title} />
|
||||
<Dropdown
|
||||
class="h-full w-[25%]"
|
||||
size="xl"
|
||||
@@ -414,11 +419,7 @@
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex items-center">
|
||||
<Vote
|
||||
value={question.votes}
|
||||
upVote={() => upvoteQuestion('question')}
|
||||
disabled={voted.question}
|
||||
/>
|
||||
<Vote value={question.votes} upVote={() => upvoteQuestion('question')} disabled={voted.question} />
|
||||
<h2 class="text-3xl">{question.title}</h2>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -444,12 +445,7 @@
|
||||
<div class="border-1 border-gray my-1 rounded-lg border px-1">
|
||||
<header class="flex items-center justify-between p-2 leading-none">
|
||||
<div class="flex items-center text-black no-underline hover:underline">
|
||||
<Avatar
|
||||
src={question.author.avatar}
|
||||
name={question.author.name}
|
||||
width="w-7"
|
||||
height="h-7"
|
||||
/>
|
||||
<Avatar src={question.author.avatar} name={question.author.name} width="w-7" height="h-7" />
|
||||
<p class="ml-2 text-sm dark:text-white">{question.author.name}</p>
|
||||
<p class="ml-2 text-sm text-gray-500 dark:text-white">
|
||||
{question.createdAt}
|
||||
@@ -459,11 +455,13 @@
|
||||
<IconButton
|
||||
value="delete-question"
|
||||
onClick={() => {
|
||||
if (!question) return;
|
||||
|
||||
deleteQuestion.shouldDelete = true;
|
||||
deleteQuestion.questionId = question.id;
|
||||
}}
|
||||
>
|
||||
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</header>
|
||||
@@ -504,7 +502,7 @@
|
||||
</div>
|
||||
|
||||
{#if isValidAnswer}
|
||||
<CheckmarkOutlineIcon size={20} />
|
||||
<CircleCheckIcon size={16} />
|
||||
{/if}
|
||||
|
||||
{#if comment.authorId === $profile.id || $isOrgAdmin}
|
||||
@@ -515,7 +513,7 @@
|
||||
deleteComment.commentId = comment.id;
|
||||
}}
|
||||
>
|
||||
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
@@ -15,24 +15,28 @@
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { Dropdown } from 'carbon-components-svelte';
|
||||
import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
|
||||
let errors: {
|
||||
title?: string;
|
||||
courseId?: string;
|
||||
body?: string;
|
||||
} = {};
|
||||
let fields = {
|
||||
} = $state({});
|
||||
let fields = $state({
|
||||
title: '',
|
||||
body: '',
|
||||
courseId: ''
|
||||
};
|
||||
});
|
||||
|
||||
let fetchedCourses: Course[] = [];
|
||||
let hasFetched = $state(false);
|
||||
let fetchedCourses: Course[] = $state([]);
|
||||
|
||||
async function getCourses(userId: string | null, orgId: string) {
|
||||
if (hasFetched) return;
|
||||
|
||||
if ($courses.length) {
|
||||
fetchedCourses = [...$courses];
|
||||
hasFetched = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,6 +44,7 @@
|
||||
if (!coursesResults) return;
|
||||
|
||||
fetchedCourses = coursesResults.allCourses;
|
||||
hasFetched = true;
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
@@ -75,11 +80,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
$effect(() => {
|
||||
if ($profile.id && $currentOrg.id) {
|
||||
getCourses($profile.id, $currentOrg.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -88,20 +93,13 @@
|
||||
|
||||
<section class="w-full md:mx-auto md:max-w-3xl">
|
||||
<div class="p-5">
|
||||
<a
|
||||
class="text-md flex items-center text-gray-500 dark:text-white"
|
||||
href={`${$currentOrgPath}/community`}
|
||||
>
|
||||
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" />
|
||||
<a class="text-md flex items-center text-gray-500 dark:text-white" href={`${$currentOrgPath}/community`}>
|
||||
<ArrowLeftIcon size={16} />
|
||||
{$t('community.ask.go_back')}
|
||||
</a>
|
||||
<div class="flex items-center justify-between gap-12">
|
||||
<h1 class="text-2xl font-bold dark:text-white md:text-3xl">{$t('community.ask.ask_the')}</h1>
|
||||
<PrimaryButton
|
||||
label={$t('community.ask.publish')}
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
onClick={handleSave}
|
||||
/>
|
||||
<h1 class="text-2xl font-bold md:text-3xl dark:text-white">{$t('community.ask.ask_the')}</h1>
|
||||
<PrimaryButton label={$t('community.ask.publish')} variant={VARIANTS.CONTAINED_DARK} onClick={handleSave} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,27 +7,28 @@
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { courses, courseMetaDeta } from '$lib/components/Courses/store';
|
||||
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
||||
import { Add } from 'carbon-icons-svelte';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { browser } from '$app/environment';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import Grid from 'carbon-icons-svelte/lib/Grid.svelte';
|
||||
import List from 'carbon-icons-svelte/lib/List.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import GridIcon from '@lucide/svelte/icons/grid-2x2';
|
||||
import ListIcon from '@lucide/svelte/icons/list';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let data;
|
||||
let { data } = $props();
|
||||
|
||||
let { cantFetch } = data;
|
||||
let searchValue = '';
|
||||
let selectedId: string;
|
||||
let filteredCourses: Course[];
|
||||
let searchValue = $state('');
|
||||
let selectedId: string = $state('0');
|
||||
let hasFetched = false;
|
||||
|
||||
const filteredCourses: Course[] = $derived(filterCourses(searchValue, selectedId, $courses));
|
||||
|
||||
async function getCourses(userId: string | undefined, orgId: string) {
|
||||
if (cantFetch && typeof cantFetch === 'boolean' && orgId && !hasFetched) {
|
||||
// only show is loading when fetching for the first time
|
||||
@@ -56,7 +57,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
filteredCourses = courses.filter((course) => {
|
||||
const filteredCourses = courses.filter((course) => {
|
||||
if (!searchValue || course.title.toLowerCase().includes(searchValue.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
@@ -65,14 +66,14 @@
|
||||
});
|
||||
|
||||
if (_selectedId === '0') {
|
||||
filteredCourses = filteredCourses.sort(
|
||||
(a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
||||
);
|
||||
return filteredCourses.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
||||
} else if (_selectedId === '1') {
|
||||
filteredCourses = filteredCourses.sort((a, b) => b.is_published - a.is_published);
|
||||
return filteredCourses.sort((a, b) => (b.is_published ? 0 : 1) - (a.is_published ? 0 : 1));
|
||||
} else if (_selectedId === '2') {
|
||||
filteredCourses = filteredCourses.sort((a, b) => b.total_lessons - a.total_lessons);
|
||||
return filteredCourses.sort((a, b) => (b.total_lessons ?? 0) - (a.total_lessons ?? 0));
|
||||
}
|
||||
|
||||
return filteredCourses;
|
||||
}
|
||||
|
||||
const setViewPreference = (preference: 'grid' | 'list') => {
|
||||
@@ -92,21 +93,22 @@
|
||||
}
|
||||
});
|
||||
|
||||
$: filterCourses(searchValue, selectedId, $courses);
|
||||
$: getCourses($profile.id, $currentOrg.id);
|
||||
$effect(() => {
|
||||
getCourses($profile.id, $currentOrg.id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Courses - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full md:max-w-6xl md:mx-auto">
|
||||
<div class="py-2 md:py-10 px-2 md:px-5">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">{$t('courses.heading')}</h1>
|
||||
<section class="w-full md:mx-auto md:max-w-6xl">
|
||||
<div class="px-2 py-2 md:px-5 md:py-10">
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold md:text-3xl dark:text-white">{$t('courses.heading')}</h1>
|
||||
{#if $isMobile}
|
||||
<PrimaryButton isDisabled={!$isOrgAdmin} onClick={openNewCourseModal}>
|
||||
<Add size={24} />
|
||||
<PlusIcon size={16} />
|
||||
</PrimaryButton>
|
||||
{:else}
|
||||
<PrimaryButton
|
||||
@@ -117,7 +119,7 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row-reverse mb-5">
|
||||
<div class="mb-5 flex flex-row-reverse">
|
||||
<div class="filter-containter flex items-end justify-start">
|
||||
<Search
|
||||
placeholder={$t('courses.search_placeholder')}
|
||||
@@ -136,18 +138,18 @@
|
||||
/>
|
||||
{#if $courseMetaDeta.view === 'list'}
|
||||
<IconButton onClick={() => setViewPreference('grid')}>
|
||||
<Grid size={24} />
|
||||
<GridIcon size={16} />
|
||||
</IconButton>
|
||||
{:else}
|
||||
<IconButton onClick={() => setViewPreference('list')}>
|
||||
<List size={24} />
|
||||
<ListIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NewCourseModal />
|
||||
<Courses bind:courses={filteredCourses} />
|
||||
<Courses courses={filteredCourses} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -5,20 +5,20 @@
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { createQuizModal } from '$lib/utils/store/org';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { Add } from 'carbon-icons-svelte';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Interactive Quizzes - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full max-w-6xl mx-auto">
|
||||
<div class="py-10 px-5">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">Interactive Quizzes</h1>
|
||||
<section class="mx-auto w-full max-w-6xl">
|
||||
<div class="px-5 py-10">
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold md:text-3xl dark:text-white">Interactive Quizzes</h1>
|
||||
{#if $isMobile}
|
||||
<PrimaryButton onClick={() => ($createQuizModal.open = true)}>
|
||||
<Add size={24} />
|
||||
<PlusIcon size={16} />
|
||||
</PrimaryButton>
|
||||
{:else}
|
||||
<PrimaryButton label="Create Quiz" onClick={() => ($createQuizModal.open = true)} />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
|
||||
import CheckmarkFilledIcon from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
|
||||
import WarningFilledIcon from 'carbon-icons-svelte/lib/WarningFilled.svelte';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
import CircleAlertIcon from '@lucide/svelte/icons/circle-alert';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import isBoolean from 'lodash/isBoolean';
|
||||
import { onMount } from 'svelte';
|
||||
@@ -32,34 +32,36 @@
|
||||
options: QuizOption[];
|
||||
}
|
||||
|
||||
export let data;
|
||||
let { data } = $props();
|
||||
const { quizId } = data;
|
||||
|
||||
// Questionnaire State
|
||||
let currentQuestion: QuizQuestion = $quizStore.questions[0] || {
|
||||
id: 0,
|
||||
label: '',
|
||||
type: 'multichoice',
|
||||
options: []
|
||||
};
|
||||
let currentQuestion: QuizQuestion = $state(
|
||||
$quizStore.questions[0] || {
|
||||
id: 0,
|
||||
label: '',
|
||||
type: 'multichoice',
|
||||
options: []
|
||||
}
|
||||
);
|
||||
|
||||
// Behavioural State
|
||||
let openPreview = false;
|
||||
let type = 'multichoice';
|
||||
let openPreview = $state(false);
|
||||
let type = $state('multichoice');
|
||||
let errors: Array<{
|
||||
isLabelEmpty: boolean;
|
||||
hasOneAnswer: boolean;
|
||||
id: number;
|
||||
options: Array<{ id: number; error: boolean }>;
|
||||
}> = [];
|
||||
}> = $state([]);
|
||||
let currentError: {
|
||||
isLabelEmpty?: boolean;
|
||||
hasOneAnswer?: boolean;
|
||||
id?: number;
|
||||
options?: Array<{ id: number; error: boolean }>;
|
||||
} = {};
|
||||
let isFocused = false;
|
||||
let selectEl: Select | null = null;
|
||||
} = $state({});
|
||||
let isFocused = $state(false);
|
||||
let selectEl: Select | null = $state(null);
|
||||
|
||||
function activeClass(q, cq) {
|
||||
if (q.id === cq.id) {
|
||||
@@ -200,9 +202,7 @@
|
||||
|
||||
function qHasError(qId, _errs) {
|
||||
return _errs.some((qe) => {
|
||||
return (
|
||||
(qId ? qe.id === qId : true) && (qe.isLabelEmpty || !qe.hasOneAnswer || !!qe.options.length)
|
||||
);
|
||||
return (qId ? qe.id === qId : true) && (qe.isLabelEmpty || !qe.hasOneAnswer || !!qe.options.length);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -239,11 +239,8 @@
|
||||
<!-- Questions list -->
|
||||
<aside class="root h-full w-1/5 bg-gray-100 p-4 dark:bg-neutral-800">
|
||||
<div class="flex h-full flex-col">
|
||||
<a
|
||||
class="text-md flex items-center text-gray-500 dark:text-white"
|
||||
href={`${$currentOrgPath}/quiz`}
|
||||
>
|
||||
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" /> Back to Quizzes
|
||||
<a class="text-md flex items-center text-gray-500 dark:text-white" href={`${$currentOrgPath}/quiz`}>
|
||||
<ArrowLeftIcon size={16} /> Back to Quizzes
|
||||
</a>
|
||||
|
||||
<h3 class="my-3">Quiz</h3>
|
||||
@@ -255,11 +252,11 @@
|
||||
question,
|
||||
currentQuestion
|
||||
)}"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
currentQuestion = question;
|
||||
type = question.type;
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
currentQuestion = question;
|
||||
type = question.type;
|
||||
@@ -269,7 +266,7 @@
|
||||
Question {i + 1}
|
||||
|
||||
{#if qHasError(question.id, errors)}
|
||||
<WarningFilledIcon size={20} class="carbon-icon error" />
|
||||
<CircleAlertIcon size={16} class="filled" />
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
@@ -325,18 +322,8 @@
|
||||
<aside class="settings h-full w-1/5 bg-gray-100 p-4 dark:bg-neutral-800">
|
||||
<div class="py-5">
|
||||
<h5>Quiz settings</h5>
|
||||
<PrimaryButton
|
||||
label="Save Changes"
|
||||
variant={VARIANTS.CONTAINED}
|
||||
onClick={saveQuiz}
|
||||
className="my-3"
|
||||
/>
|
||||
<PrimaryButton
|
||||
label="Preview Quiz"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={previewQuiz}
|
||||
className="my-3"
|
||||
/>
|
||||
<PrimaryButton label="Save Changes" variant={VARIANTS.CONTAINED} onClick={saveQuiz} className="my-3" />
|
||||
<PrimaryButton label="Preview Quiz" variant={VARIANTS.OUTLINED} onClick={previewQuiz} className="my-3" />
|
||||
<PrimaryButton
|
||||
label="Delete question"
|
||||
variant={VARIANTS.TEXT}
|
||||
@@ -377,11 +364,7 @@
|
||||
</Select>
|
||||
|
||||
<!-- -->
|
||||
<Select
|
||||
labelText="Time limit"
|
||||
bind:selected={$quizStore.timelimit}
|
||||
class="mb-3 flex items-center"
|
||||
>
|
||||
<Select labelText="Time limit" bind:selected={$quizStore.timelimit} class="mb-3 flex items-center">
|
||||
<SelectItem value="10 seconds" text="10s" />
|
||||
<SelectItem value="20 seconds" text="20s" />
|
||||
<SelectItem value="30 seconds" text="30s" />
|
||||
@@ -397,23 +380,19 @@
|
||||
|
||||
{#each allThemes as _theme}
|
||||
<div
|
||||
class="theme relative mb-5 w-full cursor-pointer rounded-md border {$quizStore.theme ===
|
||||
_theme.id && 'border-primary-700'}"
|
||||
class="theme relative mb-5 w-full cursor-pointer rounded-md border {$quizStore.theme === _theme.id &&
|
||||
'border-primary-700'}"
|
||||
>
|
||||
{#if $quizStore.theme === _theme.id}
|
||||
<CheckmarkFilledIcon
|
||||
size={24}
|
||||
class="carbon-icon absolute right-4 top-4"
|
||||
style="fill:white;"
|
||||
/>
|
||||
<CircleCheckIcon size={16} filled />
|
||||
{/if}
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="flex h-full w-full flex-col-reverse rounded-md border"
|
||||
style="background: url({themeImages[_theme.id]?.card});"
|
||||
on:click={() => ($quizStore.theme = _theme.id)}
|
||||
on:keydown={(e) => {
|
||||
onclick={() => ($quizStore.theme = _theme.id)}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
$quizStore.theme = _theme.id;
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<title>Settings - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full md:max-w-4xl mx-auto">
|
||||
<div class="py-10 px-5">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">{$t('settings.heading')}</h1>
|
||||
<section class="mx-auto w-full md:max-w-4xl">
|
||||
<div class="px-5 py-10">
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-3xl font-bold dark:text-white">{$t('settings.heading')}</h1>
|
||||
|
||||
<ViewSiteBtn />
|
||||
</div>
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
<title>Customize LMS - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full md:max-w-4xl mx-auto">
|
||||
<div class="py-10 px-3 md:px-5">
|
||||
<a class="text-gray-500 dark:text-white text-md" href={`${$currentOrgPath}/settings?tab=org`}
|
||||
<section class="mx-auto w-full md:max-w-4xl">
|
||||
<div class="px-3 py-10 md:px-5">
|
||||
<a class="text-md text-gray-500 dark:text-white" href={`${$currentOrgPath}/settings?tab=org`}
|
||||
>{$t('upgrade.back')}</a
|
||||
>
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-3xl font-bold dark:text-white">
|
||||
{$t('components.settings.customize_lms.title')}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
<title>Custom domain - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full md:max-w-4xl mx-auto">
|
||||
<div class="py-10 px-3 md:px-5">
|
||||
<a class="text-gray-500 dark:text-white text-md" href={`${$currentOrgPath}/settings?tab=org`}
|
||||
<section class="mx-auto w-full md:max-w-4xl">
|
||||
<div class="px-3 py-10 md:px-5">
|
||||
<a class="text-md text-gray-500 dark:text-white" href={`${$currentOrgPath}/settings?tab=org`}
|
||||
>{$t('upgrade.back')}</a
|
||||
>
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">{$t('components.settings.domains.title')}</h1>
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-3xl font-bold dark:text-white">{$t('components.settings.domains.title')}</h1>
|
||||
</div>
|
||||
|
||||
<Domains />
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
<title>Teams - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full md:max-w-4xl mx-auto">
|
||||
<div class="py-10 px-3 md:px-5">
|
||||
<a class="text-gray-500 dark:text-white text-md" href={`${$currentOrgPath}/settings?tab=org`}
|
||||
<section class="mx-auto w-full md:max-w-4xl">
|
||||
<div class="px-3 py-10 md:px-5">
|
||||
<a class="text-md text-gray-500 dark:text-white" href={`${$currentOrgPath}/settings?tab=org`}
|
||||
>{$t('upgrade.back')}</a
|
||||
>
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">{$t('upgrade.team_members')}</h1>
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-3xl font-bold dark:text-white">{$t('upgrade.team_members')}</h1>
|
||||
</div>
|
||||
|
||||
<UpgradeBanner>{$t('upgrade.team')}</UpgradeBanner>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import Chip from '$lib/components/Chip/Text.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import CheckmarkOutline from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -10,9 +10,17 @@
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
export let data;
|
||||
let setupList = data.setup || [];
|
||||
let completed = 0;
|
||||
let { data } = $props();
|
||||
const setupList = $derived(
|
||||
(data.setup || []).map((item) => {
|
||||
if (item.id === 'profile') {
|
||||
item.is_completed = !$profile.avatar_url.includes('avatars/avatar.png');
|
||||
}
|
||||
return item;
|
||||
})
|
||||
);
|
||||
|
||||
const completed = $derived(setupList.filter((list) => list.is_completed).length);
|
||||
|
||||
const StepsEnum = {
|
||||
UPDATE_PROFILE: 'profile',
|
||||
@@ -70,21 +78,12 @@
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
$: setupList = setupList.map((item) => {
|
||||
if (item.id === 'profile') {
|
||||
item.is_completed = !$profile.avatar_url.includes('avatars/avatar.png');
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
$: completed = setupList.filter((list) => list.is_completed).length;
|
||||
</script>
|
||||
|
||||
<section class="w-full md:max-w-4xl mx-auto">
|
||||
<div class="py-2 md:py-10 px-2 md:px-5">
|
||||
<section class="mx-auto w-full md:max-w-4xl">
|
||||
<div class="px-2 py-2 md:px-5 md:py-10">
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">{$t('setup.get_started')}</h1>
|
||||
<h1 class="text-2xl font-bold md:text-3xl dark:text-white">{$t('setup.get_started')}</h1>
|
||||
<Chip
|
||||
value={`${completed}/${setupList.length}`}
|
||||
className="text-[10px] font-semibold px-3 !py-1"
|
||||
@@ -95,18 +94,14 @@
|
||||
<section class="px-4">
|
||||
{#each setupList as list, i}
|
||||
<div
|
||||
class="flex flex-col lg:flex-row lg:items-center justify-between gap-2 w-full py-8 border-b border-gray-300"
|
||||
class="flex w-full flex-col justify-between gap-2 border-b border-gray-300 py-8 lg:flex-row lg:items-center"
|
||||
>
|
||||
<div class={`space-y-1 flex-1 ${list.is_completed ? 'opacity-50' : ''} lg:max-w-[50%]`}>
|
||||
<div class={`flex-1 space-y-1 ${list.is_completed ? 'opacity-50' : ''} lg:max-w-[50%]`}>
|
||||
<div class="flex items-center gap-3">
|
||||
<Chip
|
||||
value={i + 1}
|
||||
className={`text-[10px] font-semibold !py-1 `}
|
||||
shape="rounded-full"
|
||||
/>
|
||||
<p class="font-medium text-lg">{$t(list.title)}</p>
|
||||
<Chip value={i + 1} className={`text-[10px] font-semibold !py-1 `} shape="rounded-full" />
|
||||
<p class="text-lg font-medium">{$t(list.title)}</p>
|
||||
</div>
|
||||
<p class={`text-sm`}>
|
||||
<p class="text-sm">
|
||||
{$t(list.desc)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -118,7 +113,7 @@
|
||||
isDisabled={list.is_completed}
|
||||
>
|
||||
{#if list.is_completed}
|
||||
<CheckmarkOutline />
|
||||
<CircleCheckIcon size={16} />
|
||||
{/if}
|
||||
{$t(list.button_label)}
|
||||
</PrimaryButton>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import get from 'lodash/get';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
|
||||
const supabase = getSupabase();
|
||||
|
||||
@@ -53,9 +53,7 @@ async function getGenericData(orgSlug: string): Promise<{
|
||||
};
|
||||
}
|
||||
|
||||
async function getIsLessonCreated(
|
||||
orgSlug: string
|
||||
): Promise<{ isLessonCreated: boolean; lessonData: any }> {
|
||||
async function getIsLessonCreated(orgSlug: string): Promise<{ isLessonCreated: boolean; lessonData: any }> {
|
||||
const { data } = await supabase
|
||||
.from('lesson')
|
||||
.select(
|
||||
@@ -104,9 +102,7 @@ async function getIsExerciseCreated(orgSlug: string): Promise<boolean> {
|
||||
}
|
||||
|
||||
export const load = async ({ params = { slug: '' } }) => {
|
||||
const { isCourseCreated, isCoursePublished, orgHasAvatarUrl, courseData } = await getGenericData(
|
||||
params.slug
|
||||
);
|
||||
const { isCourseCreated, isCoursePublished, orgHasAvatarUrl, courseData } = await getGenericData(params.slug);
|
||||
const { isLessonCreated, lessonData } = await getIsLessonCreated(params.slug);
|
||||
const isExerciseCreated = await getIsExerciseCreated(params.slug);
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export const load = ({ params = { id: '' } }) => {
|
||||
return {
|
||||
profileId: params.id
|
||||
};
|
||||
};
|
||||
@@ -1,214 +0,0 @@
|
||||
<script>
|
||||
import UserAvatarIcon from 'carbon-icons-svelte/lib/UserAvatar.svelte';
|
||||
import BookIcon from 'carbon-icons-svelte/lib/Book.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { user, profile } from '$lib/utils/store/user';
|
||||
|
||||
export let data;
|
||||
const { profileId } = data;
|
||||
|
||||
let avatar;
|
||||
let loading = false;
|
||||
let currentProfile = {};
|
||||
let isOwner = false;
|
||||
let initialValueOfUserName, initialValueOfFullName, initialValueOfAvatar;
|
||||
|
||||
async function getProfile(profileId) {
|
||||
loading = true;
|
||||
// Get user profile
|
||||
|
||||
// Check if user has profile
|
||||
let {
|
||||
data: profileData,
|
||||
error,
|
||||
status
|
||||
} = await supabase.from('profile').select(`*`).eq('id', profileId).single();
|
||||
|
||||
if (error && !profileData && status === 406 && $user.currentSession) {
|
||||
// Not found
|
||||
} else if (profileData) {
|
||||
// Profile exists, go to profile page
|
||||
currentProfile = profileData;
|
||||
initialValueOfUserName = currentProfile.username;
|
||||
initialValueOfFullName = currentProfile.fullname;
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
|
||||
async function updateProfile() {
|
||||
try {
|
||||
loading = true;
|
||||
|
||||
const updates = {
|
||||
fullname: currentProfile.fullname,
|
||||
username: currentProfile.username
|
||||
};
|
||||
|
||||
if (avatar) {
|
||||
const filename = `user/${currentProfile.username + Date.now()}.webp`;
|
||||
|
||||
const { data } = await supabase.storage.from('avatars').upload(filename, avatar, {
|
||||
cacheControl: '3600',
|
||||
upsert: false
|
||||
});
|
||||
|
||||
if (data) {
|
||||
const { data: response } = supabase.storage.from('avatars').getPublicUrl(filename);
|
||||
|
||||
updates.avatar_url = response.publicUrl;
|
||||
currentProfile.avatar_url = response.publicUrl;
|
||||
}
|
||||
avatar = undefined;
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profile').update(updates).match({ id: profileId });
|
||||
|
||||
if (isOwner) {
|
||||
profile.update((_profile) => ({
|
||||
..._profile,
|
||||
...updates
|
||||
}));
|
||||
}
|
||||
|
||||
if (error) throw error;
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
loading = false;
|
||||
} finally {
|
||||
loading = false;
|
||||
|
||||
initialValueOfUserName = currentProfile.username;
|
||||
initialValueOfFullName = currentProfile.fullname;
|
||||
}
|
||||
}
|
||||
|
||||
$: isDirty =
|
||||
avatar ||
|
||||
initialValueOfUserName !== currentProfile.username ||
|
||||
initialValueOfFullName !== currentProfile.fullname;
|
||||
$: isOwner = $profile.id === profileId;
|
||||
$: getProfile(profileId);
|
||||
|
||||
$: if (avatar) {
|
||||
updateProfile();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Profile of {currentProfile.username}</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="root w-11/12 mt-3 m-auto flex items-start justify-between flex-col lg:flex-row">
|
||||
{#if currentProfile.id}
|
||||
<div class="w-full lg:w-1/4 flex items-center flex-col">
|
||||
<!-- <img
|
||||
alt={currentProfile.username}
|
||||
src={currentProfile.avatar_url}
|
||||
height="260"
|
||||
width="260"
|
||||
class="rounded-full bg-primary-100 mb-4"
|
||||
/> -->
|
||||
<UploadImage bind:avatar src={currentProfile.avatar_url} widthHeight="w-60 h-60" />
|
||||
|
||||
{#if isOwner}
|
||||
<TextField
|
||||
label="Full name"
|
||||
bind:value={currentProfile.fullname}
|
||||
className="w-60 mb-4"
|
||||
inputClassName="rounded-md"
|
||||
/>
|
||||
<TextField
|
||||
label="Username"
|
||||
bind:value={currentProfile.username}
|
||||
className="w-60 mb-4"
|
||||
inputClassName="rounded-md"
|
||||
/>
|
||||
<TextField
|
||||
label="Email"
|
||||
bind:value={currentProfile.email}
|
||||
className="w-60 mb-4"
|
||||
inputClassName="rounded-md"
|
||||
isDisabled={true}
|
||||
/>
|
||||
|
||||
{#if isDirty}
|
||||
<PrimaryButton
|
||||
label={loading ? 'Updating...' : 'Update profile'}
|
||||
onClick={updateProfile}
|
||||
isDisabled={loading}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<h3 class="dark:text-white text-xl">{currentProfile.username}</h3>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex-grow" />
|
||||
<div class="flex-grow flex flex-wrap mx-3 w-3/5">
|
||||
<div class="mb-3 w-full">
|
||||
<h3 class="dark:text-white font-bold text-md m-0">Organizations</h3>
|
||||
<p class="dark:text-white text-gray-500 text-sm">This are the organizations you work at</p>
|
||||
|
||||
<div class="hidden my-3 py-3 px-4 border shadow-sm rounded-md">
|
||||
<h4 class="dark:text-white m-0 font-normal">
|
||||
<a class="text-primary-700 font-bold" href="/courses"> CitiBim </a>
|
||||
</h4>
|
||||
<p class="dark:text-white text-gray-500">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Non aliquam tenetur magnam.
|
||||
Alias cumque quaerat expedita dolores placeat laboriosam culpa, suscipit veritatis
|
||||
adipisci eius magni obcaecati. Sequi doloremque blanditiis earum.
|
||||
</p>
|
||||
<div class="mt-2 flex items-center">
|
||||
<span class="flex mr-2 items-center">
|
||||
<UserAvatarIcon size={16} class="mr-2" /> 5
|
||||
</span>
|
||||
|
||||
<span class="flex mr-2 items-center">
|
||||
<BookIcon size={16} class="mr-2" /> 5
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden my-2 py-3 px-4 border shadow-sm rounded-md">
|
||||
<h4 class="dark:text-white m-0 font-normal">
|
||||
<a class="text-primary-700 font-bold" href="/courses"> Climate Change Group </a>
|
||||
</h4>
|
||||
<p class="dark:text-white text-gray-500">
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Illo quasi autem quo possimus
|
||||
illum consequuntur repudiandae nobis error, porro
|
||||
</p>
|
||||
<div class="mt-2 flex items-center">
|
||||
<span class="flex mr-2 items-center">
|
||||
<UserAvatarIcon size={16} class="mr-2" /> 30
|
||||
</span>
|
||||
|
||||
<span class="flex mr-2 items-center">
|
||||
<BookIcon size={16} class="mr-2" /> 3
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- {:else if loading}
|
||||
<Box>
|
||||
<Chasing size="60" color="#ff3e00" unit="px" duration="1s" />
|
||||
</Box>
|
||||
{:else}
|
||||
<Box>
|
||||
<div>
|
||||
<Search32 />
|
||||
</div>
|
||||
<h3 class="dark:text-white text-xxl">Profile not found</h3>
|
||||
</Box> -->
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
max-width: 1204px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +1,27 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import {
|
||||
resetValidation,
|
||||
getConfirmPasswordError,
|
||||
getDisableSubmit
|
||||
} from '$lib/utils/functions/validator';
|
||||
import { resetValidation, getConfirmPasswordError, getDisableSubmit } from '$lib/utils/functions/validator';
|
||||
import { RESET_FIELDS } from '$lib/utils/constants/authentication';
|
||||
import AuthUI from '$lib/components/AuthUI/index.svelte';
|
||||
|
||||
let supabase = getSupabase();
|
||||
let fields = Object.assign({}, RESET_FIELDS);
|
||||
let loading = false;
|
||||
let success = false;
|
||||
let errors = {};
|
||||
let submitError;
|
||||
let disableSubmit = false;
|
||||
let formRef;
|
||||
let fields = $state(Object.assign({}, RESET_FIELDS));
|
||||
let loading = $state(false);
|
||||
let errors: {
|
||||
email?: string;
|
||||
password?: string;
|
||||
confirmPassword?: string;
|
||||
} = $state({});
|
||||
let submitError = $state();
|
||||
let formRef: HTMLFormElement | undefined = $state();
|
||||
|
||||
async function handleSubmit(e) {
|
||||
const disableSubmit = $derived(getDisableSubmit(fields));
|
||||
|
||||
async function handleSubmit() {
|
||||
errors = resetValidation(fields);
|
||||
console.log('errors', errors);
|
||||
|
||||
@@ -40,14 +41,21 @@
|
||||
|
||||
return goto('/login');
|
||||
} catch (error) {
|
||||
submitError = error.error_description || error.message;
|
||||
submitError = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
$: errors.confirmPassword = getConfirmPasswordError(fields);
|
||||
$: disableSubmit = getDisableSubmit(fields);
|
||||
function setConfirmPasswordError(fields) {
|
||||
untrack(() => {
|
||||
errors.confirmPassword = getConfirmPasswordError(fields);
|
||||
});
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
setConfirmPasswordError(fields);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -64,8 +72,8 @@
|
||||
bind:formRef
|
||||
>
|
||||
<div class="mt-4 w-full">
|
||||
<h3 class="dark:text-white text-xl font-semibold my-3">New Password</h3>
|
||||
<p class="dark:text-white text-sm mb-6">Enter your new password details</p>
|
||||
<h3 class="my-3 text-xl font-semibold dark:text-white">New Password</h3>
|
||||
<p class="mb-6 text-sm dark:text-white">Enter your new password details</p>
|
||||
<TextField
|
||||
label="Your Password"
|
||||
bind:value={fields.password}
|
||||
@@ -94,7 +102,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="my-4 w-full flex justify-end items-center">
|
||||
<div class="my-4 flex w-full items-center justify-end">
|
||||
<PrimaryButton
|
||||
label="Reset Password"
|
||||
type="submit"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import AuthUI from '$lib/components/AuthUI/index.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
@@ -8,32 +9,29 @@
|
||||
import { SIGNUP_FIELDS } from '$lib/utils/constants/authentication';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import {
|
||||
authValidation,
|
||||
getConfirmPasswordError,
|
||||
getDisableSubmit
|
||||
} from '$lib/utils/functions/validator';
|
||||
import { authValidation, getConfirmPasswordError, getDisableSubmit } from '$lib/utils/functions/validator';
|
||||
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
|
||||
let supabase = getSupabase();
|
||||
let fields = Object.assign({}, SIGNUP_FIELDS);
|
||||
let loading = false;
|
||||
let fields = $state(Object.assign({}, SIGNUP_FIELDS));
|
||||
let loading = $state(false);
|
||||
let success = false;
|
||||
let errors: {
|
||||
email?: string;
|
||||
password?: string;
|
||||
confirmPassword?: string;
|
||||
} = {};
|
||||
let submitError: string;
|
||||
let disableSubmit = false;
|
||||
let formRef: HTMLFormElement;
|
||||
} = $state({});
|
||||
let submitError: string = $state('');
|
||||
let formRef: HTMLFormElement | undefined = $state();
|
||||
|
||||
let query = new URLSearchParams($page.url.search);
|
||||
let query = new URLSearchParams(page.url.search);
|
||||
let redirect = query.get('redirect');
|
||||
|
||||
const disableSubmit = $derived(getDisableSubmit(fields));
|
||||
|
||||
async function handleSubmit() {
|
||||
const validationRes = authValidation(fields);
|
||||
console.log('validationRes', validationRes);
|
||||
@@ -119,8 +117,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: errors.confirmPassword = getConfirmPasswordError(fields);
|
||||
$: disableSubmit = getDisableSubmit(fields);
|
||||
function setConfirmPasswordError(fields: typeof SIGNUP_FIELDS) {
|
||||
untrack(() => {
|
||||
errors.confirmPassword = getConfirmPasswordError(fields);
|
||||
});
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
setConfirmPasswordError(fields);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
||||
Reference in New Issue
Block a user