This commit is contained in:
Rotimi Best
2023-09-16 22:27:21 +01:00
78 changed files with 1838 additions and 819 deletions
+3 -3
View File
@@ -81,7 +81,7 @@
console.log('Get user profile', authUser);
if (!authUser) {
goto('/login');
goto('/login?redirect=/' + path);
}
// Check if user has profile
@@ -315,14 +315,14 @@
{#if !isQuizPage($page.url.pathname)}
<OrgSideBar />
{/if}
<div class="org-slot w-full bg-white dark:bg-gray-800">
<div class="org-slot bg-white dark:bg-gray-800 w-full">
<slot />
</div>
</div>
{:else if isLMSPage($page.url.pathname)}
<div class="org-root w-full flex items-center justify-between">
<LMSSideBar />
<div class="org-slot w-full bg-white dark:bg-gray-800">
<div class="org-slot bg-white dark:bg-gray-800 w-full">
<slot />
</div>
</div>
@@ -0,0 +1,60 @@
import { render } from 'svelte-email';
import { json } from '@sveltejs/kit';
import JoinRequestTemplate from './template.svelte';
import { getSendgrid, SENDGRID_FROM_NOREPLY } from '$lib/utils/services/sendgrid';
import { getSupabase } from '$lib/utils/functions/supabase';
const sendgrid = getSendgrid();
const supabase = getSupabase();
export async function POST({ 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
);
if (!to || !courseName || !teacherEmail || !studentFullname || !orgName) {
return json({ success: false, message: 'Missing required fields' }, { status: 400 });
}
let user;
try {
const { data } = await supabase.auth.getUser(accessToken);
user = data.user;
} catch (error) {
console.error(error);
}
if (!user) {
return json({ success: false, message: 'Unauthenticated user' }, { status: 401 });
}
const options = {
from: SENDGRID_FROM_NOREPLY,
to,
replyTo: teacherEmail,
subject: `[${courseName}] One more step left`,
html: render({
template: JoinRequestTemplate,
props: {
courseName,
studentFullname,
teacherEmail,
orgName
}
})
};
sendgrid.send(options);
return json({
success: true,
message: 'Email sent'
});
}
@@ -0,0 +1,37 @@
<script lang="ts">
import { Text } from 'svelte-email';
export let courseName = '';
export let studentFullname = '';
export let teacherEmail = '';
export let orgName = '';
const fontFamily =
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif';
const paragraph = {
fontFamily,
fontSize: '16px',
lineHeight: '26px'
};
const paragraphContent = {
...paragraph,
margin: '20px 0'
};
const footerParagraph = {
...paragraph,
margin: '0px'
};
</script>
<Text style={paragraph}>Hi {studentFullname},</Text>
<Text style={paragraphContent}>
You are one step closer to joining: {courseName}
</Text>
<Text style={paragraphContent}>
Please send your proof of payment to: {teacherEmail}, in order to join the course.
</Text>
<Text style={footerParagraph}>Talk to you soon and see you in class.</Text>
<Text style={footerParagraph}>{orgName}</Text>
@@ -0,0 +1,19 @@
<script lang="ts">
import EmailTemplate from '$lib/components/Email/EmailTemplate.svelte';
import ProvePayment from './prove_payment.svelte';
export let courseName = '';
export let studentFullname = '';
export let orgName = '';
export let teacherEmail = '';
</script>
<EmailTemplate
children={ProvePayment}
props={{
courseName,
studentFullname,
orgName,
teacherEmail
}}
/>
@@ -0,0 +1,51 @@
import { render } from 'svelte-email';
import { json } from '@sveltejs/kit';
import WelcomeTemplate from './template.svelte';
import { getSendgrid, SENDGRID_FROM_NOREPLY } from '$lib/utils/services/sendgrid';
import { getSupabase } from '$lib/utils/functions/supabase';
const sendgrid = getSendgrid();
const supabase = getSupabase();
// Sends email to student when they successfully join a course
export async function POST({ request }) {
const { to, orgName, courseName } = await request.json();
const accessToken = request.headers.get('Authorization') || '';
console.log('/POST api/email/course/student_welcome', to, orgName);
if (!to || !orgName || !courseName) {
return json({ success: false, message: 'Missing required fields' }, { status: 400 });
}
let user;
try {
const { data } = await supabase.auth.getUser(accessToken);
user = data.user;
} catch (error) {
console.error(error);
}
if (!user) {
return json({ success: false, message: 'Unauthenticated user' }, { status: 401 });
}
const options = {
from: SENDGRID_FROM_NOREPLY,
to,
subject: `[${orgName}] Welcome to Class`,
html: render({
template: WelcomeTemplate,
props: {
orgName,
courseName
}
})
};
sendgrid.send(options);
return json({
success: true,
message: 'Email sent'
});
}
@@ -0,0 +1,15 @@
<script lang="ts">
import EmailTemplate from '$lib/components/Email/EmailTemplate.svelte';
import Welcome from './welcome.svelte';
export let orgName = '';
export let courseName = '';
</script>
<EmailTemplate
children={Welcome}
props={{
orgName,
courseName
}}
/>
@@ -0,0 +1,37 @@
<script lang="ts">
import { Text } from 'svelte-email';
export let orgName = '';
export let courseName = '';
const fontFamily =
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif';
const paragraph = {
fontFamily,
fontSize: '16px',
lineHeight: '26px'
};
const paragraphContent = {
...paragraph,
margin: '20px 0'
};
const footerParagraph = {
...paragraph,
margin: '0px'
};
</script>
<Text style={paragraph}>Hey there,</Text>
<Text style={paragraphContent}>
Congratulations, you’ve successfully joined “{courseName}"
</Text>
<Text style={paragraphContent}>
Everything has been set up for you to have an amazing learning experience.
</Text>
<Text style={paragraphContent}>
If you run into any issues, don’t fail to reach out to your instructor(s).
</Text>
<Text style={footerParagraph}>Cheers,</Text>
<Text style={footerParagraph}>{orgName}</Text>
@@ -0,0 +1,57 @@
import { render } from 'svelte-email';
import { json } from '@sveltejs/kit';
import JoinRequestTemplate from './template.svelte';
import { getSendgrid, SENDGRID_FROM_NOREPLY } from '$lib/utils/services/sendgrid';
import { getSupabase } from '$lib/utils/functions/supabase';
const sendgrid = getSendgrid();
const supabase = getSupabase();
export async function POST({ request }) {
const { to, courseName, studentEmail, studentFullname } = await request.json();
const accessToken = request.headers.get('Authorization') || '';
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 });
}
let user;
try {
const { data } = await supabase.auth.getUser(accessToken);
user = data.user;
} catch (error) {
console.error(error);
}
if (!user) {
return json({ success: false, message: 'Unauthenticated user' }, { status: 401 });
}
const options = {
from: SENDGRID_FROM_NOREPLY,
to,
subject: `[${courseName}] Request to Join Course!`,
html: render({
template: JoinRequestTemplate,
props: {
courseName,
studentFullname,
studentEmail
}
})
};
sendgrid.send(options);
return json({
success: true,
message: 'Email sent'
});
}
@@ -0,0 +1,44 @@
<script lang="ts">
import { Text, Link } from 'svelte-email';
export let courseName = '';
export let studentFullname = '';
export let studentEmail = '';
const fontFamily =
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif';
const paragraph = {
fontFamily,
fontSize: '16px',
lineHeight: '26px'
};
const paragraphContent = {
...paragraph,
margin: '20px 0'
};
const footerParagraph = {
...paragraph,
margin: '0px'
};
const linkStyle = {
color: '#1d4ee2'
};
</script>
<Text style={paragraph}>Hi amazing tutor,</Text>
<Text style={paragraphContent}>
A new student has requested to join a course you are teaching: “{courseName}"
</Text>
<Text style={paragraphContent}>Student details</Text>
<Text style={paragraphContent}>
Name: {studentFullname}<br />
Email: {studentEmail}
</Text>
<Text style={footerParagraph}>Cheers</Text>
<Text style={footerParagraph}>
<Link style={linkStyle} href="https://classroomio.com">ClassroomIO</Link>
</Text>
@@ -0,0 +1,17 @@
<script lang="ts">
import EmailTemplate from '$lib/components/Email/EmailTemplate.svelte';
import JoinRequest from './join_request.svelte';
export let courseName = '';
export let studentFullname = '';
export let studentEmail = '';
</script>
<EmailTemplate
children={JoinRequest}
props={{
courseName,
studentFullname,
studentEmail
}}
/>
@@ -0,0 +1,49 @@
import { render } from 'svelte-email';
import { json } from '@sveltejs/kit';
import WelcomeTemplate from './template.svelte';
import { getSendgrid, SENDGRID_FROM_NOREPLY } from '$lib/utils/services/sendgrid';
import { getSupabase } from '$lib/utils/functions/supabase';
const sendgrid = getSendgrid();
const supabase = getSupabase();
export async function POST({ request }) {
const { to, courseName } = await request.json();
const accessToken = request.headers.get('Authorization') || '';
console.log('/POST api/email/course/teacher_student_joined', to, courseName);
if (!to || !courseName) {
return json({ success: false, message: 'Missing required fields' }, { status: 400 });
}
let user;
try {
const { data } = await supabase.auth.getUser(accessToken);
user = data.user;
} catch (error) {
console.error(error);
}
if (!user) {
return json({ success: false, message: 'Unauthenticated user' }, { status: 401 });
}
const options = {
from: SENDGRID_FROM_NOREPLY,
to,
subject: `[${courseName}] You've got a new student!`,
html: render({
template: WelcomeTemplate,
props: {
courseName
}
})
};
sendgrid.send(options);
return json({
success: true,
message: 'Email sent'
});
}
@@ -0,0 +1,13 @@
<script lang="ts">
import EmailTemplate from '$lib/components/Email/EmailTemplate.svelte';
import Welcome from './welcome.svelte';
export let courseName = '';
</script>
<EmailTemplate
children={Welcome}
props={{
courseName
}}
/>
@@ -0,0 +1,44 @@
<script lang="ts">
import { Text, Link } from 'svelte-email';
export let courseName = '';
const fontFamily =
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif';
const paragraph = {
fontFamily,
fontSize: '16px',
lineHeight: '26px'
};
const paragraphContent = {
...paragraph,
margin: '20px 0'
};
const footerParagraph = {
...paragraph,
margin: '0px'
};
const linkStyle = {
color: '#1d4ee2'
};
</script>
<Text style={paragraph}>Hi amazing tutor,</Text>
<Text style={paragraphContent}>
Congratulations, a new student has joined a course you are teaching: “{courseName}"
</Text>
<Text style={paragraphContent}>
We hope they have a great experience learning from the best (YOU).
</Text>
<Text style={paragraphContent}>
If you run into any issues, please don’t fail to reach out to us, we’d love to make your teaching
experience as easy as possible.
</Text>
<Text style={footerParagraph}>Cheers</Text>
<Text style={footerParagraph}>
<Link style={linkStyle} href="https://classroomio.com">ClassroomIO</Link>
</Text>
@@ -1,7 +1,7 @@
import { render } from 'svelte-email';
import { json } from '@sveltejs/kit';
import WelcomeTemplate from './template.svelte';
import { getSendgrid, SENDGRID_FROM_NOTIFY } from '$lib/utils/services/sendgrid';
import { getSendgrid, SENDGRID_FROM_NOREPLY } from '$lib/utils/services/sendgrid';
import { getSupabase } from '$lib/utils/functions/supabase';
const sendgrid = getSendgrid();
@@ -10,7 +10,7 @@ const supabase = getSupabase();
export async function POST({ request }) {
const { to, name, orgName, courseName, orgSiteName } = await request.json();
const accessToken = request.headers.get('Authorization') || '';
console.log('/POST api/email/course/welcome', to, name, orgName);
console.log('/POST api/email/course/teacher_welcome', to, name, orgName);
if (!to || !name || !orgName || !courseName || !orgSiteName) {
return json({ success: false, message: 'Missing required fields' }, { status: 400 });
@@ -32,7 +32,7 @@ export async function POST({ request }) {
const inviteLink = `${origin}/org/${orgSiteName}/courses`;
const options = {
from: SENDGRID_FROM_NOTIFY,
from: SENDGRID_FROM_NOREPLY,
to,
subject: `You have been invited to a course in ${orgName}!`,
html: render({
+4 -4
View File
@@ -1,7 +1,7 @@
import { render } from 'svelte-email';
import { json } from '@sveltejs/kit';
import TeacherInviteTemplate from './template.svelte';
import { getSendgrid, SENDGRID_FROM_NOTIFY } from '$lib/utils/services/sendgrid';
import { getSendgrid, SENDGRID_FROM_NOREPLY } from '$lib/utils/services/sendgrid';
import { getSupabase } from '$lib/utils/functions/supabase';
const sendgrid = getSendgrid();
@@ -15,7 +15,7 @@ export async function POST({ request }) {
const accessToken = request.headers.get('Authorization');
console.log('/POST api/email/invite', body);
if (!org || !Object.keys(org) || !email) {
if (!org || !Object.keys(org) || !email || !accessToken) {
return json(
{ success: false, message: 'Org data and Teacher name are required' },
{ status: 400 }
@@ -43,11 +43,11 @@ export async function POST({ request }) {
orgId: id,
orgSiteName: siteName
});
const inviteLink = `${origin}/invite/${encodeURIComponent(btoa(inviteData))}`;
const inviteLink = `${origin}/invite/t/${encodeURIComponent(btoa(inviteData))}`;
console.log('inviteData', inviteData);
const options = {
from: SENDGRID_FROM_NOTIFY,
from: SENDGRID_FROM_NOREPLY,
to: email,
subject: `Join ${name} on ClassroomIO`,
html: render({
+11 -1
View File
@@ -1,5 +1,15 @@
export const load = ({ params = { slug: '' } }) => {
import { fetchCourse } from '$lib/utils/services/courses';
import { supabase, getSupabase } from '$lib/utils/functions/supabase';
if (!supabase) {
getSupabase();
}
export const load = async ({ params = { slug: '' } }) => {
const { data } = await fetchCourse(undefined, params.slug);
return {
slug: params.slug,
course: data
};
};
+13 -31
View File
@@ -1,39 +1,21 @@
<script>
import Backdrop from '$lib/components/Backdrop/index.svelte';
import { Moon } from 'svelte-loading-spinners';
<script lang="ts">
import CourseLandingPage from '$lib/components/CourseLandingPage/index.svelte';
import { profile } from '$lib/utils/store/user';
import { setCourse, course } from '$lib/components/Course/store';
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
import { fetchCourse } from '$lib/utils/services/courses';
import { browser } from '$app/environment';
import type { Course } from '$lib/utils/types';
export let data;
interface Data {
course: Course;
}
export let data: Data;
let courseData = {};
let loading = false;
let courseData: Course;
async function triggerSetCourse(_profile) {
if (!browser) return;
loading = true;
const { data: _data } = await fetchCourse(undefined, data.slug);
setCourse(_data);
loading = false;
function setCourseData(course: Course | null) {
if (!course) return;
courseData = { ...course };
}
function setCourseData(course, lessons) {
courseData = { ...course, lessons };
console.log('courseData', courseData);
}
$: triggerSetCourse($profile);
$: setCourseData($course, $lessons);
$: setCourseData(data.course);
</script>
{#if loading}
<Backdrop>
<Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
</Backdrop>
{:else}
<CourseLandingPage {courseData} />
{/if}
<CourseLandingPage {courseData} />
+70 -67
View File
@@ -138,77 +138,80 @@
<CourseContainer bind:isStudent>
<PageNav title="Attendance" />
<section class="mx-9 my-5">
<div class="flex flex-row items-center justify-between">
<div class="flex">
<p class="flex items-center mr-5">
<Checkbox checked disabled /> Present
</p>
<p class="flex items-center">
<Checkbox disabled /> Absent
</p>
<PageBody width="w-full max-w-6xl md:w-11/12">
<section class="flex items-center mx-2 lg:mx-9 my-5">
<div
class="flex flex-col lg:flex-row items-start gap-2 lg:items-center justify-between w-full"
>
<div class="flex">
<p class="flex items-center mr-5">
<Checkbox checked disabled /> Present
</p>
<p class="flex items-center">
<Checkbox disabled /> Absent
</p>
</div>
<div>
<Search
class="dark:text-slate-950"
placeholder="Search students"
bind:value={searchValue}
/>
</div>
</div>
<div>
<Search
class="dark:text-slate-950"
placeholder="Search students"
bind:value={searchValue}
/>
</div>
</div>
</section>
</section>
<section class="my-5 mx-9">
<StructuredList class="m-0">
<!-- Moved the lesson headers outside the students loop -->
<StructuredListHead class="bg-primary-100">
<StructuredListRow head class="mx-7">
<StructuredListCell head class="text-primary-600 py-3">Student</StructuredListCell>
{#each $lessons as lesson, index}
<StructuredListCell head class="text-primary-600 py-3"
>Lesson 0{getLectureNo(index + 1)}</StructuredListCell
>
{/each}
</StructuredListRow>
</StructuredListHead>
{#each searchStudents(searchValue) as student}
<StructuredListBody>
<StructuredListRow>
<StructuredListCell>
<div class="w-1/4 flex items-center">
<p class="dark:text-white font-semibold">
{student.profile.fullname}
</p>
</div>
</StructuredListCell>
{#each $lessons as lesson}
<StructuredListCell class="">
<Checkbox
class={isStudent && 'cursor-not-allowed'}
disabled={isStudent}
checked={$attendance[student.id]
? $attendance[student.id][lesson.id]
? $attendance[student.id][lesson.id].is_present
: false
: false}
on:change={(e) => handleAttendanceChange(e, student, lesson)}
/>
</StructuredListCell>
<section class="my-5 mx-2 lg:mx-9">
<StructuredList class="m-0 relative">
<!-- Moved the lesson headers outside the students loop -->
<StructuredListHead class="bg-primary-100">
<StructuredListRow head class="mx-7">
<StructuredListCell head class="text-primary-600 py-3">Student</StructuredListCell>
{#each $lessons as lesson, index}
<StructuredListCell head class="text-primary-600 py-3"
>Lesson 0{getLectureNo(index + 1)}</StructuredListCell
>
{/each}
</StructuredListRow>
</StructuredListBody>
{/each}
</StructuredList>
{#if students.length === 0}
<div>
<Box>
</StructuredListHead>
{#each searchStudents(searchValue) as student}
<StructuredListBody>
<StructuredListRow>
<StructuredListCell>
<div class="w-1/4 flex items-center">
<p class="dark:text-white font-semibold">
{student.profile.fullname}
</p>
</div>
</StructuredListCell>
{#each $lessons as lesson}
<StructuredListCell class="">
<Checkbox
class={isStudent && 'cursor-not-allowed'}
disabled={isStudent}
checked={$attendance[student.id]
? $attendance[student.id][lesson.id]
? $attendance[student.id][lesson.id].is_present
: false
: false}
on:change={(e) => handleAttendanceChange(e, student, lesson)}
/>
</StructuredListCell>
{/each}
</StructuredListRow>
</StructuredListBody>
{/each}
</StructuredList>
{#if students.length === 0}
<Box className="h-[300px] w-full">
<AudioConsoleIcon size={32} class="carbon-icon w-80" />
<h3 class="text-3xl text-gray-500 dark:text-white">No Student Added</h3>
<h3 class="text-3xl text-gray-500 dark:text-white text-center">No Student Added</h3>
</Box>
</div>
{/if}
<Pagination totalItems={10} pageSizes={[10, 15, 20]} />
</section>
{/if}
{#if students.length !== 0}
<Pagination totalItems={10} pageSizes={[10, 15, 20]} />
{/if}
</section>
</PageBody>
</CourseContainer>
@@ -30,26 +30,21 @@
</script>
<div
class="root absolute inset-0 flex justify-center items-center z-50 bg-white"
class="absolute inset-0 z-50 bg-white"
in:fly={{ y: 500, duration: 500 }}
out:fly={{ y: 500, duration: 500 }}
>
<Editor {courseId} bind:course={courseData} />
<div class="rightBar" class:isMobile={$isMobile}>
<CourseLandingPage bind:courseData />
<div class="rightBar">
<CourseLandingPage bind:courseData editMode={true} />
</div>
</div>
<style>
.rightBar {
flex-grow: 1;
width: calc(100% - 360px);
overflow: auto;
width: 100%;
height: 100%;
overflow: auto;
}
.rightBar.isMobile {
margin-left: 10px;
overflow-y: auto;
}
</style>
+2 -2
View File
@@ -321,12 +321,12 @@
</div>
{:else}
<Box>
<div class="flex justify-between flex-col items-center w-96">
<div class="flex justify-between flex-col items-center w-[90%] md:w-96">
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="my-2.5 mx-auto" />
<h2 class="text-xl my-1.5 font-normal">No lessons yet</h2>
<p class="text-sm text-center text-slate-500">
Share your knowledge with the world by creating engaging lessons. Start by clicking on
the New Lesson button.
the Add button.
</p>
</div>
</Box>
@@ -4,17 +4,19 @@
import { fetchCourse, fetchLesson } from '$lib/utils/services/courses';
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants.js';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { Loading } from 'carbon-components-svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte';
import Materials from '$lib/components/Course/components/Lesson/Materials/index.svelte';
import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte';
import MODES from '$lib/utils/constants/mode.js';
import MODES from '$lib/utils/constants/mode';
import { setCourse, course } from '$lib/components/Course/store';
import Download from 'carbon-icons-svelte/lib/Download.svelte';
import OverflowMenuVertical from 'carbon-icons-svelte/lib/OverflowMenuVertical.svelte';
import { apps } from '$lib/components/Apps/store';
import IconButton from '$lib/components/IconButton/index.svelte';
import {
lesson,
lessons,
@@ -144,6 +146,10 @@
}));
}
const toggleApps = () => {
$apps.open = !$apps.open;
};
function toggleMode() {
prevMode = mode;
mode = mode === MODES.edit ? MODES.view : MODES.edit;
@@ -176,9 +182,20 @@
<svelte:fragment slot="widget">
<RoleBasedSecurity allowedRoles={[1, 2]}>
{#if data.isMaterialsTabActive}
<div class="flex items-center">
<div class="lg:hidden">
<IconButton onClick={toggleApps} buttonClassName="">
<OverflowMenuVertical size={24} />
</IconButton>
</div>
<div
class={`flex-row ${
$apps.dropdown && $apps.open
? 'absolute lg:relative top-[85px] lg:top-0 right-14 lg:right-0 z-40 dark:bg-gray-800 p-3 lg:p-0'
: 'hidden'
} lg:flex items-center`}
>
<PrimaryButton
className="mr-2"
className="mb-2 lg:mb-0 mr-2"
variant={VARIANTS.OUTLINED}
onClick={downloadLesson}
{isLoading}
@@ -207,7 +224,7 @@
<PageBody>
<Materials lessonId={data.lessonId} {mode} {prevMode} {toggleMode} bind:isSaving />
<div class="w-full flex flex-row-reverse">
<div class="w-full flex flex-row-reverse mt-10">
<PrimaryButton
onClick={markLessonComplete}
isLoading={isMarkingComplete}
+15 -33
View File
@@ -8,10 +8,7 @@
import { ROLE } from '$lib/utils/constants/roles';
import { setCourse, course, group } from '$lib/components/Course/store';
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
import {
fetchCourse,
fetchExercisesByMarks,
} from '$lib/utils/services/courses';
import { fetchCourse, fetchExercisesByMarks } from '$lib/utils/services/courses';
import { getLectureNo } from '$lib/components/Course/function';
import { fetchMarks } from '$lib/utils/services/marks';
import { profile } from '$lib/utils/store/user';
@@ -19,10 +16,8 @@
export let data;
const { courseId } = data;
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 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 = [];
let isStudent = false;
@@ -52,11 +47,11 @@
if (studentMarksByExerciseId[groupmember_id]) {
studentMarksByExerciseId[groupmember_id] = {
...studentMarksByExerciseId[groupmember_id],
[exercise_id]: total_points_gotten,
[exercise_id]: total_points_gotten
};
} else {
studentMarksByExerciseId[groupmember_id] = {
[exercise_id]: total_points_gotten,
[exercise_id]: total_points_gotten
};
}
});
@@ -70,11 +65,11 @@
if (lessonMapping[lesson_id]) {
lessonMapping[lesson_id] = {
...lessonMapping[lesson_id],
[exercise_id]: { title: exercise_title, points },
[exercise_id]: { title: exercise_title, points }
};
} else {
lessonMapping[lesson_id] = {
[exercise_id]: { title: exercise_title, points },
[exercise_id]: { title: exercise_title, points }
};
}
});
@@ -82,20 +77,16 @@
$: {
students = isStudent
? $group.people.filter(
(person) => !!person.profile && person.profile.id === $profile.id
)
: $group.people.filter(
(person) => !!person.profile && person.role_id === ROLE.STUDENT
);
? $group.people.filter((person) => !!person.profile && person.profile.id === $profile.id)
: $group.people.filter((person) => !!person.profile && person.role_id === ROLE.STUDENT);
}
</script>
<CourseContainer bind:isStudent>
<PageNav title="Marks" />
<PageBody width="w-11/12">
<div class="table rounded-md border border-gray-300 w-auto">
<PageBody width="w-full max-w-6xl md:w-11/12">
<div class="table rounded-md border border-gray-300 w-full">
<div class="flex items-center {borderBottomGrey}">
<div class="box flex items-center p-3">
<p class="dark:text-white w-40">Student</p>
@@ -111,8 +102,7 @@
>
{#each Object.keys(lessonMapping[lesson.id]) as exerciseId, index}
<p
class="col text-sm dark:text-white {index &&
borderleftGrey}"
class="col text-sm dark:text-white {index && borderleftGrey}"
title={lessonMapping[lesson.id][exerciseId].title}
>
{lessonMapping[lesson.id][exerciseId].title}
@@ -129,9 +119,7 @@
</div>
{#each students as student}
<div
class="flex relative items-center p-3 cursor-pointer {borderBottomGrey}"
>
<div class="flex relative items-center p-3 cursor-pointer {borderBottomGrey}">
<div class="w-40 flex items-center">
<img
alt="Student avatar"
@@ -143,11 +131,7 @@
{student.profile.fullname}
</p>
<p class="dark:text-white">
{`${
student.assigned_student_id
? '#' + student.assigned_student_id
: '-'
}`}
{`${student.assigned_student_id ? '#' + student.assigned_student_id : '-'}`}
</p>
</div>
</div>
@@ -176,9 +160,7 @@
{:else}
<Box>
<AudioConsoleIcon size={32} class="carbon-icon w-80" />
<h3 class="text-3xl text-gray-500 dark:text-white">
No Student Added
</h3>
<h3 class="text-3xl text-gray-500 dark:text-white">No Student Added</h3>
</Box>
{/each}
</div>
+141 -129
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import TextChip from '$lib/components/Chip/Text.svelte';
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
import Filter from 'carbon-icons-svelte/lib/Filter.svelte';
import { onMount } from 'svelte';
import copy from 'copy-to-clipboard';
import {
@@ -17,6 +18,7 @@
import { fetchCourse, deleteGroupMember, updatedGroupMember } from '$lib/utils/services/courses';
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import Select from '$lib/components/Form/Select.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
@@ -135,138 +137,148 @@
</RoleBasedSecurity>
</slot:fragment>
</PageNav>
<section class="my-5 mx-9">
<div class="flex flex-row flex-end justify-end items-center mb-7">
<div class="mr-5 w-80">
<Search
class="dark:text-slate-950 border-0 bg-zinc-100 w-full"
placeholder="Search people"
bind:value={searchValue}
/>
</div>
<div class="mb-3">
<Select bind:value={filterBy} options={ROLES} className="dark:text-black mt-3 w-20" />
<!-- <select bind:value={filterBy} class="mt-3">
{#each ROLES as option}
<option value={option.value}>{option.label}</option>
{/each}
</select> -->
</div>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<p class="dark:text-white hidden lg:block text-lg w-20" />
</RoleBasedSecurity>
</div>
<StructuredList class="m-0">
<StructuredListHead
class="bg-slate-100 dark:bg-neutral-800 dark:border-2 dark:border-neutral-800"
<PageBody width="w-full max-w-6xl md:w-11/12">
<section class="my-5 mx-2 md:mx-9">
<div
class="flex flex-col md:flex-row flex-end gap-2 justify-end items-start md:items-center mb-7"
>
<StructuredListRow head class="mx-7">
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
>Name</StructuredListCell
>
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
>Role</StructuredListCell
>
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
>Action</StructuredListCell
>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<p class="dark:text-white hidden lg:block text-lg w-20" />
</RoleBasedSecurity>
</StructuredListRow>
</StructuredListHead>
<div class="max-w-[320px]">
<Search
class="dark:text-slate-950 border-0 bg-zinc-100 w-full"
placeholder="Search people"
bind:value={searchValue}
/>
</div>
<div class="mb-3">
<Select
bind:value={filterBy}
options={ROLES}
className="dark:text-black mt-3 max-w-[80px]"
/>
<!-- <select bind:value={filterBy} class="mt-3">
{#each ROLES as option}
<option value={option.value}>{option.label}</option>
{/each}
</select> -->
</div>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<p class="dark:text-white hidden lg:block text-lg w-20" />
</RoleBasedSecurity>
</div>
{#each filterPeople(searchValue, people) as person}
<StructuredListBody>
<StructuredListRow class="relative">
<!-- first column -->
<StructuredListCell class="w-3/6">
{#if person.profile}
<div class="flex items-center absolute bottom-3">
<Avatar
src={person.profile.avatar_url}
name={person.profile.fullname}
width="w-8"
height="h-8"
className="mr-3"
/>
<div class="flex items-center">
<div class="mr-2">
<p class="dark:text-white text-base font-normal">
{person.profile.fullname}
</p>
<p class="text-xs text-primary-600">
{getEmail(person)}
</p>
</div>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="hidden lg:flex lg:justify-between lg:items-center mr-2">
<CopyButton text={getEmail(person)} feedback="Copied Email to clipboard" />
<StructuredList class="m-0">
<StructuredListHead
class="bg-slate-100 dark:bg-neutral-800 dark:border-2 dark:border-neutral-800"
>
<StructuredListRow head class="mx-7">
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
>Name</StructuredListCell
>
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
>Role</StructuredListCell
>
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
>Action</StructuredListCell
>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<p class="dark:text-white hidden lg:block text-lg w-20" />
</RoleBasedSecurity>
</StructuredListRow>
</StructuredListHead>
{#each filterPeople(searchValue, people) as person}
<StructuredListBody>
<StructuredListRow class="relative">
<!-- first column -->
<StructuredListCell class="w-4/6 md:w-3/6">
{#if person.profile}
<div class="flex items-start lg:items-center">
<Avatar
src={person.profile.avatar_url}
name={person.profile.fullname}
width="w-8"
height="h-8"
className="mr-3"
/>
<div class="flex flex-col lg:flex-row items-start lg:items-center">
<div class="mr-2">
<p class="dark:text-white text-base font-normal">
{person.profile.fullname}
</p>
<p class="text-xs text-primary-600 line-clamp-1">
{getEmail(person)}
</p>
</div>
</RoleBasedSecurity>
{#if person.profile_id == $profile.id}
<ComingSoon label="You" />
<div class="flex items-center">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<CopyButton
text={getEmail(person)}
feedback="Copied Email to clipboard"
/>
</RoleBasedSecurity>
{#if person.profile_id == $profile.id}
<ComingSoon label="You" />
{/if}
</div>
</div>
</div>
{:else}
<div class="flex items-start lg:items-center w-2/4">
<TextChip
value={person.email.substring(0, 2).toUpperCase()}
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"
>
{person.email}
</a>
<div class="flex items-center justify-between">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<CopyButton text={getEmail(person)} feedback="Copied Email to clipboard" />
</RoleBasedSecurity>
<TextChip
value="Pending"
className="text-xs bg-yellow-200 text-yellow-700 h-fit"
size="sm"
/>
</div>
</div>
{/if}
</StructuredListCell>
<!-- second column -->
<StructuredListCell class="w-1/4 px-3">
<p class=" dark:text-white font-normal text-center text-base w-1/4">
{ROLE_LABEL[person.role_id]}
</p>
</StructuredListCell>
<!-- third column -->
<StructuredListCell class="p-0 w-1/4">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="hidden lg:flex lg:justify-between lg:items-center w-20">
{#if person.profile_id !== $profile.id}
<IconButton
onClick={() => {
member = person;
$deleteMemberModal.open = true;
}}
>
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
</IconButton>
{/if}
</div>
</div>
{:else}
<div class="flex items-center w-2/4 absolute bottom-3">
<TextChip
value={person.email.substring(0, 2).toUpperCase()}
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"
>
{person.email}
</a>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="hidden lg:flex lg:justify-between lg:items-center w-20">
<CopyButton text={getEmail(person)} feedback="Copied Email to clipboard" />
</div>
</RoleBasedSecurity>
<TextChip
value="Pending"
className="text-xs bg-yellow-200 text-yellow-700 h-fit"
size="sm"
/>
</div>
{/if}
</StructuredListCell>
<!-- second column -->
<StructuredListCell class="w-1/4 py-4 px-3">
<p class="dark:text-white font-normal text-base w-1/4 my-auto">
{ROLE_LABEL[person.role_id]}
</p>
</StructuredListCell>
<!-- third column -->
<StructuredListCell class="p-0 w-1/4">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="hidden lg:flex lg:justify-between lg:items-center w-20">
{#if person.profile_id !== $profile.id}
<IconButton
onClick={() => {
member = person;
$deleteMemberModal.open = true;
}}
>
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
</IconButton>
{/if}
</div>
</RoleBasedSecurity>
</StructuredListCell>
</StructuredListRow>
</StructuredListBody>
{/each}
</StructuredList>
<Pagination totalItems={10} pageSizes={[10, 15, 20]} />
</section>
</RoleBasedSecurity>
</StructuredListCell>
</StructuredListRow>
</StructuredListBody>
{/each}
</StructuredList>
<!-- <Pagination totalItems={10} pageSizes={[10, 15, 20]} /> -->
</section>
</PageBody>
</CourseContainer>
@@ -244,8 +244,8 @@
<CourseContainer>
<PageNav title="Submitted Exercises" />
<PageBody width="w-11/12 overflow-x-auto overflow-y-hidden">
<div class="flex items-center w-full">
<PageBody width="w-full max-w-6xl md:w-11/12 overflow-x-auto">
<div class="flex items-center w-full overflow-x-scroll">
{#each sections as { id, title, items }, idx (id)}
<div
class="section rounded-md bg-gray-100 dark:bg-gray-800 border border-gray-50 p-3 h-80 mr-3 overflow-hidden"
@@ -301,7 +301,7 @@
{`${item.tutor ? 'created by' + item.tutor.name : ''}`}
</p>
</a>
<p class="dark:text-white text-gray-500 dark:text-white text-xs">
<p class="dark:text-white text-gray-500 text-xs">
{item.submittedAt}
</p>
<!-- <div class="badge rounded-md px-2 bg-green-500 text-white">
@@ -324,10 +324,15 @@
.section {
max-width: 355px;
min-width: 355px;
height: 80vh;
height: 75vh;
}
.content {
height: 95%;
}
@media screen and (max-width: 768px) {
.section {
min-width: 250px;
}
}
</style>
@@ -0,0 +1,31 @@
import { redirect } from '@sveltejs/kit';
import { getSupabase, supabase } from '$lib/utils/functions/supabase';
import { getCurrentOrg } from '$lib/utils/services/org';
if (!supabase) {
getSupabase();
}
export const load = async ({ params = { hash: '' } }) => {
try {
const courseHashData = atob(decodeURIComponent(params.hash));
console.log('courseHashData', courseHashData);
const { groupId, name, description, orgSiteName } = JSON.parse(courseHashData);
if (!groupId || !name || !description || !orgSiteName) {
throw 'Validation failed';
}
const currentOrg = await getCurrentOrg(orgSiteName, true);
return {
groupId,
name,
description,
currentOrg
};
} catch (error) {
console.error('Error decoding course invite params.hash', error);
throw redirect(301, '/404');
}
};
+125
View File
@@ -0,0 +1,125 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { getSupabase } from '$lib/utils/functions/supabase';
import AuthUI from '$lib/components/AuthUI/index.svelte';
import { currentOrg } from '$lib/utils/store/org';
import { setTheme } from '$lib/utils/functions/theme';
import { addGroupMember } from '$lib/utils/services/courses';
import type { CurrentOrg } from '$lib/utils/types/org.js';
import { ROLE } from '$lib/utils/constants/roles';
import { profile } from '$lib/utils/store/user.js';
import { sendStudentCourseWelcome, sendTeacherStudentJoinedCourse } from './utils';
export let data;
let supabase = getSupabase();
let loading = false;
let refresh = false;
let disableSubmit = false;
let formRef: HTMLFormElement;
async function handleSubmit() {
loading = true;
if (refresh) {
window.location.reload();
refresh = false;
return;
}
if (!$profile.id || !$profile.email) {
console.log('Profile not found', $profile);
refresh = true;
return;
}
const member = {
profile_id: $profile.id,
group_id: data.groupId,
role_id: ROLE.STUDENT
};
const teacherMembers = await supabase
.from('groupmember')
.select('id, profile(email)')
.eq('group_id', data.groupId)
.eq('role_id', ROLE.TUTOR);
const teachers: Array<string> =
teacherMembers.data?.map((teacher) => {
return teacher.profile?.email || '';
}) || [];
addGroupMember(member).then((addedMember) => {
loading = false;
if (addedMember.error) {
console.error('Error adding student to group', data.groupId);
return;
}
// Send email welcoming student to the course
sendStudentCourseWelcome({
to: $profile.email,
orgName: data.currentOrg?.name,
courseName: data.name
});
// Send notification to teacher(s) that a student has joined the course.
sendTeacherStudentJoinedCourse({
to: teachers,
courseName: data.name
});
// go to lms
return goto('/lms');
});
}
function setCurOrg(cOrg: CurrentOrg) {
if (!cOrg) return;
currentOrg.set(cOrg);
}
onMount(async () => {
setTheme(data.currentOrg?.theme || '');
});
$: setCurOrg(data.currentOrg as CurrentOrg);
</script>
<svelte:head>
<title>Join {data.name} on ClassroomIO</title>
</svelte:head>
<AuthUI
{supabase}
isLogin={false}
{handleSubmit}
isLoading={loading}
showOnlyContent={true}
showLogo={true}
bind:formRef
>
<div class="mt-0 w-full">
{#if refresh}
<p class="dark:text-white text-sm font-light text-center">
Something went wrong, please try again.
</p>
{:else}
<h3 class="dark:text-white text-lg font-medium mt-0 mb-4 text-center">{data.name}</h3>
<p class="dark:text-white text-sm font-light text-center">{data.description}</p>
{/if}
</div>
<div class="my-4 w-full flex justify-center items-center">
<PrimaryButton
label={refresh ? 'Try again' : 'Join Course'}
type="submit"
isDisabled={disableSubmit || loading}
isLoading={loading}
/>
</div>
</AuthUI>
+56
View File
@@ -0,0 +1,56 @@
import { supabase } from '$lib/utils/functions/supabase';
export async function sendStudentCourseWelcome(params: {
to: string;
orgName: string;
courseName: string;
}) {
const { data } = await supabase.auth.getSession();
const accessToken = data.session?.access_token || '';
try {
const response = await fetch('/api/email/course/student_welcome', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: accessToken
},
body: JSON.stringify(params)
});
if (!response.ok) {
throw new Error(await response.text());
}
const result = await response.json();
console.log('Sent invite email', result);
} catch (error) {
console.error('Error sending invite email', error);
}
}
export async function sendTeacherStudentJoinedCourse(params: {
to: Array<string>;
courseName: string;
}) {
const { data } = await supabase.auth.getSession();
const accessToken = data.session?.access_token || '';
try {
const response = await fetch('/api/email/course/teacher_student_joined', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: accessToken
},
body: JSON.stringify(params)
});
if (!response.ok) {
throw new Error(await response.text());
}
const result = await response.json();
console.log('Sent invite email', result);
} catch (error) {
console.error('Error sending invite email', error);
}
}
@@ -1,7 +1,14 @@
import { redirect } from '@sveltejs/kit';
import { getCurrentOrg } from '$lib/utils/services/org';
import { getSupabase } from '$lib/utils/functions/supabase';
import { getSupabase, supabase } from '$lib/utils/functions/supabase';
import { getProfile } from '$lib/utils/functions/user';
if (!supabase) {
getSupabase();
}
// we need to know if the email exists or not.
// with this we can only ask the user to accept
export const load = async ({ params = { hash: '' } }) => {
try {
const hashData = atob(decodeURIComponent(params.hash));
@@ -9,13 +16,16 @@ export const load = async ({ params = { hash: '' } }) => {
const { orgId, email, orgSiteName } = JSON.parse(hashData);
getSupabase();
const _currentOrg = await getCurrentOrg(orgSiteName, true);
const profile = await getProfile(email);
console.log('profile data', profile);
return {
orgId,
email,
currentOrg: _currentOrg
currentOrg: _currentOrg,
profile
};
} catch (error) {
console.error('Error decoding invite params.hash', error);
@@ -10,11 +10,11 @@
} from '$lib/utils/functions/validator';
import { SIGNUP_FIELDS } from '$lib/utils/constants/authentication';
import AuthUI from '$lib/components/AuthUI/index.svelte';
import { currentOrg } from '$lib/utils/store/org';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
import { setTheme } from '$lib/utils/functions/theme';
import type { CurrentOrg } from '$lib/utils/types/org.js';
import { onMount } from 'svelte';
import Error from '../../+error.svelte';
import type { Profile } from '$lib/components/Course/components/People/types';
export let data;
@@ -34,7 +34,10 @@
let formRef: HTMLFormElement;
async function handleSubmit() {
const validationRes = authValidation(fields);
const validationRes = authValidation({
...fields,
email: 'test@gmail.com' // validation for this ema
});
console.log('validationRes', validationRes);
if (Object.keys(validationRes).length) {
@@ -44,40 +47,44 @@
try {
loading = true;
// Signup
const { data: signupData, error } = await supabase.auth.signUp({
email: data.email,
password: fields.password
});
let profile: Profile | null = data.profile;
console.log('Signup', signupData);
if (error) throw error;
if (!data.profile) {
// Signup
const { data: signupData, error } = await supabase.auth.signUp({
email: data.email,
password: fields.password
});
if (!signupData.user) {
throw 'User cannot be created ';
console.log('Signup', signupData);
if (error) throw error;
if (!signupData.user) {
throw 'User cannot be created ';
}
// Insert profile
const userId = signupData.user.id;
const profileRes = await supabase
.from('profile')
.insert({
id: userId,
username: fields.name.toLowerCase().replace(' ', '-') + new Date().getTime(),
fullname: fields.name,
email: data.email
})
.select();
console.log('Insert profile', profileRes.data);
if (profileRes.error) {
throw profileRes.error;
}
profile = profileRes.data[0] || {};
}
// Insert profile
const userId = signupData.user.id;
const profileRes = await supabase
.from('profile')
.insert({
id: userId,
username: fields.name.toLowerCase().replace(' ', '-') + new Date().getTime(),
fullname: fields.name,
email: data.email
})
.select();
console.log('Insert profile', profileRes.data);
if (profileRes.error) {
throw profileRes.error;
}
const profile = profileRes.data[0] || {};
if (!profile.id) {
if (!profile?.id) {
throw 'Unable to create profile';
}
@@ -91,12 +98,23 @@
.match({ email: data.email, organization_id: data.currentOrg?.id });
console.log('Update member response', updateMemberRes);
formRef?.reset();
return goto('/login');
const res = await supabase.auth.signInWithPassword({
email: data.email,
password: fields.password
});
if (res.error) {
throw res.error;
}
formRef?.reset();
return goto($currentOrgPath);
} catch (error) {
if (error instanceof Error) {
submitError = error.message;
} else {
submitError = error?.toString() || '';
}
} finally {
loading = false;
@@ -116,6 +134,7 @@
$: disableSubmit = getDisableSubmit(fields);
$: setCurOrg(data.currentOrg as CurrentOrg);
$: console.log('data.profile', data.profile);
</script>
<svelte:head>
@@ -132,7 +151,13 @@
bind:formRef
>
<div class="mt-4 w-full">
<p class="dark:text-white text-lg font-semibold mb-6">Create a free account</p>
<p class="dark:text-white text-lg font-semibold mb-6">
{#if data.profile}
Log in to join
{:else}
Create a free account to join
{/if}
</p>
<TextField
label="Your Email"
value={data.email}
@@ -142,18 +167,20 @@
inputClassName="w-full"
isDisabled={true}
/>
<TextField
label="Full Name"
bind:value={fields.name}
type="text"
autoFocus={true}
placeholder="e.g Joke Silva"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.name}
isRequired
/>
{#if !data.profile}
<TextField
label="Full Name"
bind:value={fields.name}
type="text"
autoFocus={true}
placeholder="e.g Joke Silva"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.name}
isRequired
/>
{/if}
<TextField
label="Your Password"
bind:value={fields.password}
@@ -166,17 +193,19 @@
helperMessage="Password must be more than 6 characters"
isRequired
/>
<TextField
label="Confirm Password"
bind:value={fields.confirmPassword}
type="password"
placeholder="************"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.confirmPassword}
isRequired
/>
{#if !data.profile}
<TextField
label="Confirm Password"
bind:value={fields.confirmPassword}
type="password"
placeholder="************"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.confirmPassword}
isRequired
/>
{/if}
{#if submitError}
<p class="text-sm text-red-500">{submitError}</p>
{/if}
@@ -184,7 +213,7 @@
<div class="my-4 w-full flex justify-end items-center">
<PrimaryButton
label="Create and Join"
label="Accept Invite"
type="submit"
className="sm:w-full w-full"
isDisabled={disableSubmit || loading}
+11
View File
@@ -1,4 +1,5 @@
<script>
import { page } from '$app/stores';
import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { getSupabase } from '$lib/utils/functions/supabase';
@@ -14,6 +15,9 @@
let loading = false;
let errors = {};
let query = new URLSearchParams($page.url.search);
let redirect = query.get('redirect');
async function handleSubmit() {
const validationRes = authValidation(fields);
console.log('validationRes', validationRes);
@@ -31,6 +35,13 @@
});
console.log('data', data);
if (error) throw error;
// reload page on org site cause for some reason the authlistener on +layout.svelte doesn't run
// on org site
if ($currentOrg.id && redirect) {
console.log('Forcing full page reload for auth listener');
window.location.reload();
}
} catch (error) {
submitError = error.error_description || error.message;
} finally {
-4
View File
@@ -199,10 +199,6 @@
$: progress = Math.round((step / maxSteps) * 100);
$: fields.siteName = generateSitename(fields.siteName || '');
$: setOrgSiteName(fields.orgName, isSiteNameTouched);
$: {
console.log('currentOrg', $currentOrg);
}
</script>
{#if $profile.id}
+7 -6
View File
@@ -15,6 +15,7 @@
import WelcomeModal from '$lib/components/WelcomeModal/WelcomeModal.svelte';
import { onMount } from 'svelte';
import { Add } from 'carbon-icons-svelte';
import { isOrgAdmin } from '$lib/utils/store/org';
import { VARIANTS } from '$lib/components/PrimaryButton/constants.js';
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
@@ -201,13 +202,13 @@
<div class="flex items-center justify-between mb-10">
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">Dashboard</h1>
<div class="flex items-center">
{#if $isMobile}
<PrimaryButton onClick={createCourse}>
<PrimaryButton variant={VARIANTS.OUTLINED} onClick={createCourse} isDisabled={!$isOrgAdmin}>
{#if $isMobile}
<Add size={24} />
</PrimaryButton>
{:else}
<PrimaryButton variant={VARIANTS.OUTLINED} label="Create Course" onClick={createCourse} />
{/if}
{:else}
Create Course
{/if}
</PrimaryButton>
<VisitOrgSiteButton />
</div>
+7 -2
View File
@@ -10,6 +10,7 @@
import { currentOrg } from '$lib/utils/store/org';
import { Add } from 'carbon-icons-svelte';
import { isMobile } from '$lib/utils/store/useMobile';
import { isOrgAdmin } from '$lib/utils/store/org';
export let data;
let { allCourses, cantFetch } = data;
@@ -51,11 +52,15 @@
<div class="flex items-center justify-between mb-10">
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">Courses</h1>
{#if $isMobile}
<PrimaryButton onClick={() => ($createCourseModal.open = true)}>
<PrimaryButton isDisabled={!$isOrgAdmin} onClick={() => ($createCourseModal.open = true)}>
<Add size={24} />
</PrimaryButton>
{:else}
<PrimaryButton label="Create Course" onClick={() => ($createCourseModal.open = true)} />
<PrimaryButton
label="Create Course"
isDisabled={!$isOrgAdmin}
onClick={() => ($createCourseModal.open = true)}
/>
{/if}
</div>
+55 -9
View File
@@ -1,5 +1,6 @@
<script>
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { getSupabase } from '$lib/utils/functions/supabase';
@@ -10,15 +11,20 @@
} from '$lib/utils/functions/validator';
import { SIGNUP_FIELDS } from '$lib/utils/constants/authentication';
import AuthUI from '$lib/components/AuthUI/index.svelte';
import { profile } from '$lib/utils/store/user';
import { currentOrg } from '$lib/utils/store/org';
let supabase = getSupabase();
let fields = Object.assign({}, SIGNUP_FIELDS);
let loading = false;
let success = false;
let errors = {};
let submitError;
let submitError: string;
let disableSubmit = false;
let formRef;
let formRef: HTMLFormElement;
let query = new URLSearchParams($page.url.search);
let redirect = query.get('redirect');
async function handleSubmit(e) {
const validationRes = authValidation(fields);
@@ -31,18 +37,58 @@
try {
loading = true;
const { data, error } = await supabase.auth.signUp({
const {
data: { session },
error
} = await supabase.auth.signUp({
email: fields.email,
password: fields.password
});
console.log('data', data);
console.log('session', session);
if (error) throw error;
formRef?.reset();
const { user: authUser } = session || {};
if (!authUser) {
throw 'Error creating user';
}
return goto('/login');
// success = true;
// fields = Object.assign({}, SIGNUP_FIELDS);
if (!$currentOrg.id) return;
const [regexUsernameMatch] = [...(authUser.email?.matchAll(/(.*)@/g) || [])];
const response = await fetch('https://api.ipregistry.co/?key=tryout');
const metadata = await response.json();
const profileRes = await supabase
.from('profile')
.insert({
id: authUser.id,
username: regexUsernameMatch[1] + `${new Date().getTime()}`,
fullname: regexUsernameMatch[1],
email: authUser.email,
metadata
})
.select();
console.log('profileRes', profileRes);
if (profileRes.error) {
throw profileRes.error;
}
// Setting profile
console.log('setting profile', profileRes.data[0]);
profile.set(profileRes.data[0]);
if (redirect) {
goto(redirect);
} else {
goto('/login');
}
formRef?.reset();
success = true;
fields = Object.assign({}, SIGNUP_FIELDS);
} catch (error) {
submitError = error.error_description || error.message;
} finally {