CLASSROOMIO-T-152: Landing page fully ready
This commit is contained in:
@@ -154,7 +154,7 @@
|
|||||||
$menu.hidden
|
$menu.hidden
|
||||||
? '-translate-x-[100%] absolute md:translate-x-0 md:relative z-[40]'
|
? '-translate-x-[100%] absolute md:translate-x-0 md:relative z-[40]'
|
||||||
: 'translate-x-0 absolute md:relative z-[40]'
|
: 'translate-x-0 absolute md:relative z-[40]'
|
||||||
} transition w-[250px] min-w-[250px] bg-gray-100 dark:bg-gray-800 h-[calc(100vh-48px)] border border-l-0 border-t-0 border-b-0 border-r-1`}
|
} transition w-[350px] min-w-[350px] md:max-w-[350px] bg-gray-100 dark:bg-gray-800 h-[calc(100vh-48px)] overflow-y-auto border border-l-0 border-t-0 border-b-0 border-r-1`}
|
||||||
>
|
>
|
||||||
<div class="h-full flex flex-col">
|
<div class="h-full flex flex-col">
|
||||||
<ul class="my-5">
|
<ul class="my-5">
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import TextField from '$lib/components/Form/TextField.svelte';
|
||||||
|
import Modal from '$lib/components/Modal/index.svelte';
|
||||||
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
|
import { coursePaymentValidation } from '$lib/utils/functions/validator';
|
||||||
|
import { Link } from 'carbon-components-svelte';
|
||||||
|
import { sendTeacherStudentJoinRequest, sendStudentRequestReceipt } from './utils';
|
||||||
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
|
|
||||||
|
export let open = false;
|
||||||
|
export let paymentLink = '';
|
||||||
|
export let teacherEmail = '';
|
||||||
|
export let courseName = '';
|
||||||
|
|
||||||
|
let fields = {
|
||||||
|
fullname: '',
|
||||||
|
email: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
let errors = {
|
||||||
|
fullname: '',
|
||||||
|
email: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const STEPS = {
|
||||||
|
STEP_1: 'ENTER_DETAILS',
|
||||||
|
STEP_2: 'GO_TO_PAYMENT'
|
||||||
|
};
|
||||||
|
|
||||||
|
let step = STEPS.STEP_1;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
errors = coursePaymentValidation(fields);
|
||||||
|
console.log('coursePayment errors', errors);
|
||||||
|
|
||||||
|
if (Object.keys(errors).length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paymentLink) {
|
||||||
|
step = STEPS.STEP_2;
|
||||||
|
|
||||||
|
// Send email to first added teacher of student wanting to join
|
||||||
|
sendTeacherStudentJoinRequest({
|
||||||
|
to: teacherEmail,
|
||||||
|
courseName,
|
||||||
|
studentEmail: fields.email,
|
||||||
|
studentFullname: fields.fullname
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickPaymentLink() {
|
||||||
|
// Send email to student to send proof of payment to teacher
|
||||||
|
sendStudentRequestReceipt({
|
||||||
|
to: fields.email,
|
||||||
|
courseName,
|
||||||
|
teacherEmail,
|
||||||
|
studentFullname: fields.fullname,
|
||||||
|
orgName: $currentOrg.name
|
||||||
|
});
|
||||||
|
|
||||||
|
fields.fullname = '';
|
||||||
|
fields.email = '';
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal onClose={() => (open = false)} bind:open width="w-96" modalHeading="Process course payment">
|
||||||
|
{#if step === STEPS.STEP_1}
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<TextField
|
||||||
|
label="Your Fullname"
|
||||||
|
bind:value={fields.fullname}
|
||||||
|
autoFocus={true}
|
||||||
|
placeholder="John Doe"
|
||||||
|
className="mb-4"
|
||||||
|
isRequired={true}
|
||||||
|
autoComplete={false}
|
||||||
|
errorMessage={errors.fullname}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Your Fullname"
|
||||||
|
bind:value={fields.email}
|
||||||
|
placeholder="[email protected]"
|
||||||
|
className="mb-4"
|
||||||
|
isRequired={true}
|
||||||
|
autoComplete={false}
|
||||||
|
errorMessage={errors.email}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mt-5 flex items-center flex-row-reverse">
|
||||||
|
<PrimaryButton
|
||||||
|
className="px-6 py-3"
|
||||||
|
label={paymentLink ? 'Next' : 'Finish'}
|
||||||
|
type="submit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{:else if step === STEPS.STEP_2}
|
||||||
|
<p>
|
||||||
|
You will now be taken to a payment page, once you've paid send a proof of payment to the
|
||||||
|
course admin
|
||||||
|
</p>
|
||||||
|
<div class="mt-5 flex items-center flex-row-reverse">
|
||||||
|
<Link href={paymentLink} target="_blank" on:click={onClickPaymentLink}>Go to payment</Link>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import get from 'lodash/get';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import type { Course } from '$lib/utils/types';
|
|
||||||
import getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter';
|
import getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter';
|
||||||
import { isCourseFree } from '$lib/utils/functions/course';
|
import { isCourseFree } from '$lib/utils/functions/course';
|
||||||
import get from 'lodash/get';
|
import { getStudentInviteLink } from '$lib/utils/functions/course';
|
||||||
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import PaymentModal from './PaymentModal.svelte';
|
||||||
|
import type { Course } from '$lib/utils/types';
|
||||||
|
import { ROLE } from '$lib/utils/constants/roles';
|
||||||
|
|
||||||
export let className = '';
|
export let className = '';
|
||||||
export let editMode = false;
|
export let editMode = false;
|
||||||
export let courseData: Course = {
|
export let courseData: Course;
|
||||||
id: '',
|
export let startCoursePayment = false;
|
||||||
title: '',
|
|
||||||
description: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
let calculatedCost = 0;
|
let calculatedCost = 0;
|
||||||
let discount = 0;
|
let discount = 0;
|
||||||
@@ -29,27 +31,39 @@
|
|||||||
if (editMode) return;
|
if (editMode) return;
|
||||||
|
|
||||||
if (isFree) {
|
if (isFree) {
|
||||||
// get invite link
|
const link = getStudentInviteLink(courseData, $currentOrg.siteName);
|
||||||
// redirect to link
|
goto(link);
|
||||||
} else {
|
} else {
|
||||||
|
startCoursePayment = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buyNow() {}
|
|
||||||
|
|
||||||
function setFormatter(currency: string | undefined) {
|
function setFormatter(currency: string | undefined) {
|
||||||
if (!currency) return;
|
if (!currency) return;
|
||||||
formatter = getCurrencyFormatter(currency);
|
formatter = getCurrencyFormatter(currency);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTeacherEmail(group: Course['group']) {
|
||||||
|
const firstTutor = group?.members?.find((m) => m.role_id === ROLE.TUTOR);
|
||||||
|
|
||||||
|
return firstTutor?.profile?.email || '';
|
||||||
|
}
|
||||||
|
|
||||||
$: setFormatter(courseData.currency);
|
$: setFormatter(courseData.currency);
|
||||||
$: discount = get(courseData, 'metadata.discount', 0);
|
$: discount = get(courseData, 'metadata.discount', 0);
|
||||||
$: calculatedCost = calcDisc(discount, courseData.cost || 0);
|
$: calculatedCost = calcDisc(discount, courseData.cost || 0);
|
||||||
$: isFree = isCourseFree(calculatedCost);
|
$: isFree = isCourseFree(calculatedCost);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<PaymentModal
|
||||||
|
bind:open={startCoursePayment}
|
||||||
|
paymentLink={get(courseData, 'metadata.paymentLink', '')}
|
||||||
|
courseName={courseData.title}
|
||||||
|
teacherEmail={getTeacherEmail(courseData.group)}
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Pricing Details -->
|
<!-- Pricing Details -->
|
||||||
<aside class="{className} price-container lg:sticky lg:top-0 lg:shadow-2xl lg:rounded-lg m-h-fit">
|
<aside class="{className} price-container lg:sticky lg:top-10 lg:shadow-2xl lg:rounded-lg m-h-fit">
|
||||||
<div class="p-10">
|
<div class="p-10">
|
||||||
<!-- Pricing -->
|
<!-- Pricing -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
@@ -72,7 +86,7 @@
|
|||||||
<!-- Call To Action Buttons -->
|
<!-- Call To Action Buttons -->
|
||||||
<div class="flex flex-col w-full items-center">
|
<div class="flex flex-col w-full items-center">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
label="Join Course"
|
label={isFree ? 'Join Course' : 'Buy Now'}
|
||||||
className="w-full sm:w-full py-3 mb-3"
|
className="w-full sm:w-full py-3 mb-3"
|
||||||
onClick={handleJoinCourse}
|
onClick={handleJoinCourse}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { supabase } from '$lib/utils/functions/supabase';
|
||||||
|
|
||||||
|
export async function sendTeacherStudentJoinRequest(params: {
|
||||||
|
to: string;
|
||||||
|
courseName: string;
|
||||||
|
studentEmail: string;
|
||||||
|
studentFullname: string;
|
||||||
|
}) {
|
||||||
|
const { data } = await supabase.auth.getSession();
|
||||||
|
const accessToken = data.session?.access_token || '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/email/course/teacher_student_buycourse', {
|
||||||
|
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 sendStudentRequestReceipt(params: {
|
||||||
|
to: string;
|
||||||
|
courseName: string;
|
||||||
|
orgName: string;
|
||||||
|
studentFullname: string;
|
||||||
|
teacherEmail: string;
|
||||||
|
}) {
|
||||||
|
const { data } = await supabase.auth.getSession();
|
||||||
|
const accessToken = data.session?.access_token || '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/email/course/student_prove_payment', {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,11 +26,7 @@
|
|||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
export let editMode = false;
|
export let editMode = false;
|
||||||
export let courseData: Course = {
|
export let courseData: Course;
|
||||||
id: '',
|
|
||||||
title: '',
|
|
||||||
description: ''
|
|
||||||
};
|
|
||||||
const ratingsImg = [
|
const ratingsImg = [
|
||||||
'/images/rating-1.svg',
|
'/images/rating-1.svg',
|
||||||
'/images/rating-2.svg',
|
'/images/rating-2.svg',
|
||||||
@@ -44,6 +40,7 @@
|
|||||||
let player: any;
|
let player: any;
|
||||||
let averageRating = 0;
|
let averageRating = 0;
|
||||||
let totalRatings = 0;
|
let totalRatings = 0;
|
||||||
|
let startCoursePayment = false;
|
||||||
|
|
||||||
// initialize the expandDescription array with 'false' values for each review.
|
// initialize the expandDescription array with 'false' values for each review.
|
||||||
let expandDescription = Array(reviews.length).fill(false);
|
let expandDescription = Array(reviews.length).fill(false);
|
||||||
@@ -62,16 +59,6 @@
|
|||||||
activeNav = window.location.hash;
|
activeNav = window.location.hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addToCart() {}
|
|
||||||
|
|
||||||
function buyNow() {
|
|
||||||
// if (courseData.cost) {
|
|
||||||
// paystackApi.tra;
|
|
||||||
// } else {
|
|
||||||
// // Course is free
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
function initPlyr(_player: any, _video: string | undefined) {
|
function initPlyr(_player: any, _video: string | undefined) {
|
||||||
if (!player) return;
|
if (!player) return;
|
||||||
|
|
||||||
@@ -120,7 +107,13 @@
|
|||||||
<p class="dark:text-white author my-3 text-sm">
|
<p class="dark:text-white author my-3 text-sm">
|
||||||
{get(courseData, 'metadata.instructor.name', '')}
|
{get(courseData, 'metadata.instructor.name', '')}
|
||||||
</p>
|
</p>
|
||||||
<PrimaryButton label="Start Course" className="px-6 py-5 mt-6 sm:w-fit" />
|
<PrimaryButton
|
||||||
|
label="Start Course"
|
||||||
|
className="px-6 py-5 mt-6 sm:w-fit"
|
||||||
|
onClick={() => {
|
||||||
|
startCoursePayment = true;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Banner Image getEmbedId(videoUrl) -->
|
<!-- Banner Image getEmbedId(videoUrl) -->
|
||||||
@@ -165,16 +158,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<PricingSection className="md:hidden" {courseData} {editMode} />
|
<PricingSection className="md:hidden" {courseData} {editMode} bind:startCoursePayment />
|
||||||
<!-- Body -->
|
<!-- Body -->
|
||||||
<div class="bg-white dark:bg-gray-800 w-full">
|
<div class="bg-white dark:bg-gray-800 w-full">
|
||||||
<div
|
<div
|
||||||
class="py-8 lg:w-11/12 w-full m-2 lg:m-auto flex flex-col-reverse lg:flex-row items-center lg:items-start justify-between"
|
class="py-8 lg:w-11/12 w-full m-2 lg:m-auto flex flex-col-reverse lg:flex-row items-center lg:items-start justify-between max-w-[1200px]"
|
||||||
>
|
>
|
||||||
<!-- Course Details -->
|
<!-- Course Details -->
|
||||||
<div class="course-content w-full p-3 lg:w-10/12 lg:mr-14">
|
<div class="course-content w-full p-3 lg:w-10/12 lg:mr-14">
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav class="flex items-center border-b border-gray-300 pb-3">
|
<nav class="flex items-center border-b border-gray-300 py-3 sticky top-11 bg-white">
|
||||||
{#each NAV_ITEMS as navItem}
|
{#each NAV_ITEMS as navItem}
|
||||||
<a
|
<a
|
||||||
href="{$page.url.pathname}{navItem.key}"
|
href="{$page.url.pathname}{navItem.key}"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
export let logo;
|
export let logo;
|
||||||
export let orgName;
|
export let orgName;
|
||||||
export let isOrgSite = false;
|
export let isOrgSite = false;
|
||||||
export let backgroundColor = 'bg-blue';
|
export let backgroundColor = 'bg-white';
|
||||||
|
|
||||||
let navClass = '';
|
let navClass = '';
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,22 @@ export const authValidation = (fields = {}) => {
|
|||||||
return processErrors(error);
|
return processErrors(error);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const coursePaymentValidation = (fields = {}) => {
|
||||||
|
const schema = z.object({
|
||||||
|
fullname: z.string().min(6, {
|
||||||
|
message: 'Must be 6 or more characters long',
|
||||||
|
invalid_type_error: 'Must not be empty'
|
||||||
|
}),
|
||||||
|
email: z.string().email({
|
||||||
|
message: 'Invalid email address',
|
||||||
|
invalid_type_error: 'Must not be empty'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const { error } = schema.safeParse(fields);
|
||||||
|
|
||||||
|
return processErrors(error);
|
||||||
|
};
|
||||||
|
|
||||||
export const resetValidation = (fields = {}) => {
|
export const resetValidation = (fields = {}) => {
|
||||||
const { error } = resetValidationSchema.safeParse(fields);
|
const { error } = resetValidationSchema.safeParse(fields);
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export interface Group {
|
|||||||
organization_id?: string /* foreign key to organization.id */;
|
organization_id?: string /* foreign key to organization.id */;
|
||||||
organization?: Organization;
|
organization?: Organization;
|
||||||
tutors: Profile[];
|
tutors: Profile[];
|
||||||
|
members: Groupmember[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Role {
|
export interface Role {
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
}}
|
||||||
|
/>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
<Text style={paragraph}>Hi amazing tutor,</Text>
|
<Text style={paragraph}>Hi amazing tutor,</Text>
|
||||||
<Text style={paragraphContent}>
|
<Text style={paragraphContent}>
|
||||||
Congratulations, a new student has joined a course you are teaching: “{courseName}""
|
Congratulations, a new student has joined a course you are teaching: “{courseName}"
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={paragraphContent}>
|
<Text style={paragraphContent}>
|
||||||
We hope they have a great experience learning from the best (YOU).
|
We hope they have a great experience learning from the best (YOU).
|
||||||
|
|||||||
@@ -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 {
|
return {
|
||||||
slug: params.slug,
|
slug: params.slug,
|
||||||
|
course: data
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,39 +1,21 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import Backdrop from '$lib/components/Backdrop/index.svelte';
|
|
||||||
import { Moon } from 'svelte-loading-spinners';
|
|
||||||
import CourseLandingPage from '$lib/components/CourseLandingPage/index.svelte';
|
import CourseLandingPage from '$lib/components/CourseLandingPage/index.svelte';
|
||||||
import { profile } from '$lib/utils/store/user';
|
import type { Course } from '$lib/utils/types';
|
||||||
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';
|
|
||||||
|
|
||||||
export let data;
|
interface Data {
|
||||||
|
course: Course;
|
||||||
|
}
|
||||||
|
export let data: Data;
|
||||||
|
|
||||||
let courseData = {};
|
let courseData: Course;
|
||||||
let loading = false;
|
|
||||||
|
|
||||||
async function triggerSetCourse(_profile) {
|
function setCourseData(course: Course | null) {
|
||||||
if (!browser) return;
|
if (!course) return;
|
||||||
loading = true;
|
|
||||||
const { data: _data } = await fetchCourse(undefined, data.slug);
|
courseData = { ...course };
|
||||||
setCourse(_data);
|
|
||||||
loading = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCourseData(course, lessons) {
|
$: setCourseData(data.course);
|
||||||
courseData = { ...course, lessons };
|
|
||||||
console.log('courseData', courseData);
|
|
||||||
}
|
|
||||||
|
|
||||||
$: triggerSetCourse($profile);
|
|
||||||
$: setCourseData($course, $lessons);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loading}
|
<CourseLandingPage {courseData} />
|
||||||
<Backdrop>
|
|
||||||
<Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
|
|
||||||
</Backdrop>
|
|
||||||
{:else}
|
|
||||||
<CourseLandingPage {courseData} />
|
|
||||||
{/if}
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
let supabase = getSupabase();
|
let supabase = getSupabase();
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
let refresh = false;
|
||||||
|
|
||||||
let disableSubmit = false;
|
let disableSubmit = false;
|
||||||
let formRef: HTMLFormElement;
|
let formRef: HTMLFormElement;
|
||||||
@@ -23,6 +24,18 @@
|
|||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
loading = true;
|
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 = {
|
const member = {
|
||||||
profile_id: $profile.id,
|
profile_id: $profile.id,
|
||||||
group_id: data.groupId,
|
group_id: data.groupId,
|
||||||
@@ -91,13 +104,19 @@
|
|||||||
bind:formRef
|
bind:formRef
|
||||||
>
|
>
|
||||||
<div class="mt-0 w-full">
|
<div class="mt-0 w-full">
|
||||||
<h3 class="dark:text-white text-lg font-medium mt-0 mb-4 text-center">{data.name}</h3>
|
{#if refresh}
|
||||||
<p class="dark:text-white text-sm font-light text-center">{data.description}</p>
|
<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>
|
||||||
|
|
||||||
<div class="my-4 w-full flex justify-center items-center">
|
<div class="my-4 w-full flex justify-center items-center">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
label="Join Course"
|
label={refresh ? 'Try again' : 'Join Course'}
|
||||||
type="submit"
|
type="submit"
|
||||||
isDisabled={disableSubmit || loading}
|
isDisabled={disableSubmit || loading}
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
|
|||||||
Reference in New Issue
Block a user