Merge branch 'main' of https://gitlab.com/thenextgreat/classroomio
This commit is contained in:
@@ -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>
|
||||
+3
-3
@@ -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({
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+13
-3
@@ -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: '[email protected]' // 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}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user