CLASSROOMIO-T-152: Add payment link + reload on org login

This commit is contained in:
Rotimi Best
2023-09-14 23:04:51 +01:00
parent 530ea721a2
commit bc7a5e1e4d
16 changed files with 133 additions and 91 deletions
@@ -15,6 +15,7 @@
import { sendTeacherCourseWelcome } from './utils'; import { sendTeacherCourseWelcome } from './utils';
import type { Course } from '$lib/utils/types'; import type { Course } from '$lib/utils/types';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { getStudentInviteLink } from '$lib/utils/functions/course';
interface Tutor { interface Tutor {
id: number; id: number;
@@ -109,21 +110,6 @@
isLoadingTutors = false; isLoadingTutors = false;
} }
function getInviteLink(_course: Course, siteName: string) {
const hash = encodeURIComponent(
btoa(
JSON.stringify({
groupId: _course.group?.id,
name: _course.title,
description: _course.description,
orgSiteName: siteName
})
)
);
return `https://${siteName}.classroomio.com/invite/s/${hash}`;
}
function copyLink() { function copyLink() {
copy(link); copy(link);
copied = true; copied = true;
@@ -139,7 +125,7 @@
$: setTutors($currentOrg.id, $invitationModal.open); $: setTutors($currentOrg.id, $invitationModal.open);
$: link = getInviteLink($course, $currentOrg.siteName); $: link = getStudentInviteLink($course, $currentOrg.siteName);
</script> </script>
<Modal <Modal
+1
View File
@@ -21,6 +21,7 @@ export const course: Writable<Course> = writable({
videoUrl: '', videoUrl: '',
showDiscount: false, showDiscount: false,
discount: 0, discount: 0,
paymentLink: '',
reward: { reward: {
show: false, show: false,
description: '' description: ''
@@ -1,32 +1,36 @@
<script> <script lang="ts">
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import set from 'lodash/set'; import set from 'lodash/set';
import get from 'lodash/get'; import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import { Toggle } from 'carbon-components-svelte'; import { Toggle, Select, SelectItem } from 'carbon-components-svelte';
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import TextEditor from '$lib/components/TextEditor/index.svelte'; import TextEditor from '$lib/components/TextEditor/index.svelte';
import { isCourseFree } from '$lib/utils/functions/course';
import type { Course } from '$lib/utils/types';
export let course = {}; export let course: Course;
let discount = 0; let discount = 0;
let showDiscount; let paymentLink = '';
let giftToggled; let showDiscount = false;
let giftToggled = false;
let hasBeenSet = false; let hasBeenSet = false;
function setDefaults(course) { function setDefaults(course: Course) {
console.log('\n\nsetDefaults course', course); console.log('\n\nsetDefaults course', course);
if (isEmpty(course) || hasBeenSet) return; if (isEmpty(course) || hasBeenSet) return;
hasBeenSet = true; hasBeenSet = true;
paymentLink = get(course, 'metadata.paymentLink', '');
discount = get(course, 'metadata.discount', 0); discount = get(course, 'metadata.discount', 0);
showDiscount = get(course, 'metadata.showDiscount', false); showDiscount = get(course, 'metadata.showDiscount', false);
giftToggled = get(course, 'metadata.reward.show', false); giftToggled = get(course, 'metadata.reward.show', false);
} }
function setter(value, setterKey) { function setter(value: string | number | boolean, setterKey: string) {
console.log('\n\nsetter course', value);
if (typeof value !== 'boolean' && !value) return; if (typeof value !== 'boolean' && !value) return;
const _course = cloneDeep(course); const _course = cloneDeep(course);
@@ -34,66 +38,76 @@
course = _course; course = _course;
} }
function handleChange(html) { function handleChange(html: string) {
const _course = cloneDeep(course); const _course = cloneDeep(course);
set(_course, 'metadata.reward.description', html); set(_course, 'metadata.reward.description', html);
course = _course; course = _course;
} }
$: setter(showDiscount, 'metadata.showDiscount'); $: setter(showDiscount, 'metadata.showDiscount');
$: setter(paymentLink, 'metadata.paymentLink');
$: setter(discount, 'metadata.discount'); $: setter(discount, 'metadata.discount');
$: setter(giftToggled, 'metadata.reward.show'); $: setter(giftToggled, 'metadata.reward.show');
$: setDefaults(course); $: setDefaults(course);
</script> </script>
<TextField {#if typeof course !== 'undefined'}
className="mt-5" <Select labelText="Currency" bind:selected={course.currency}>
labelClassName="font-bold" <SelectItem value="NGN" />
label="Currency" <SelectItem value="USD" />
value={course.currency} </Select>
isDisabled={true}
/>
<TextField
className="mt-5"
labelClassName="font-bold"
label="Cost"
type="number"
bind:value={course.cost}
/>
<div class="mt-5">
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> -->
<Toggle labelText="Discount" bind:toggled={showDiscount}>
<span slot="labelA">No</span>
<span slot="labelB">Yes</span>
</Toggle>
</div>
{#if showDiscount}
<TextField <TextField
className="mt-5" className="mt-5"
labelClassName="font-bold" labelClassName="font-bold"
label="Discout Percent" label="Cost"
type="number" type="number"
bind:value={discount} bind:value={course.cost}
helperMessage="In Percentage %"
/> />
{/if}
<div class="mt-5"> {#if !isCourseFree(course.cost || 0)}
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> --> <TextField
<Toggle labelText="Gift on Completion" bind:toggled={giftToggled}> className="mt-5"
<span slot="labelA">No</span> labelClassName="font-bold"
<span slot="labelB">Yes</span> label="Payment Link"
</Toggle> helperMessage="Stripe, Lemon Squeezy or any payment link"
</div> bind:value={paymentLink}
/>
{/if}
{#if giftToggled} <div class="mt-5">
<p class="dark:text-white font-bold mt-5">About Gift</p> <!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> -->
<Toggle labelText="Discount" bind:toggled={showDiscount}>
<div class="h-2/5"> <span slot="labelA">No</span>
<TextEditor value={get(course, 'metadata.reward.description')} onChange={handleChange} /> <span slot="labelB">Yes</span>
</Toggle>
</div> </div>
{#if showDiscount}
<TextField
className="mt-5"
labelClassName="font-bold"
label="Discout Percent"
type="number"
bind:value={discount}
helperMessage="In Percentage %"
/>
{/if}
<div class="mt-5">
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> -->
<Toggle labelText="Gift on Completion" bind:toggled={giftToggled}>
<span slot="labelA">No</span>
<span slot="labelB">Yes</span>
</Toggle>
</div>
{#if giftToggled}
<p class="dark:text-white font-bold mt-5">About Gift</p>
<div class="h-2/5">
<TextEditor value={get(course, 'metadata.reward.description', '')} onChange={handleChange} />
</div>
{/if}
{/if} {/if}
@@ -1,11 +1,12 @@
<script lang="ts"> <script lang="ts">
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import type { Course } from '$lib/utils/types'; 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 get from 'lodash/get'; import get from 'lodash/get';
export let className = ''; export let className = '';
export let editMode = false;
export let courseData: Course = { export let courseData: Course = {
id: '', id: '',
title: '', title: '',
@@ -15,6 +16,7 @@
let calculatedCost = 0; let calculatedCost = 0;
let discount = 0; let discount = 0;
let formatter: Intl.NumberFormat | undefined; let formatter: Intl.NumberFormat | undefined;
let isFree = false;
function calcDisc(percent: number, cost: number) { function calcDisc(percent: number, cost: number) {
if (!percent) return cost; if (!percent) return cost;
@@ -23,7 +25,15 @@
return Math.round(discountedPrice); return Math.round(discountedPrice);
} }
function addToCart() {} function handleJoinCourse() {
if (editMode) return;
if (isFree) {
// get invite link
// redirect to link
} else {
}
}
function buyNow() {} function buyNow() {}
@@ -32,9 +42,11 @@
formatter = getCurrencyFormatter(currency); formatter = getCurrencyFormatter(currency);
} }
$: console.log('courseData.currency', courseData.currency);
$: 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);
</script> </script>
<!-- Pricing Details --> <!-- Pricing Details -->
@@ -44,6 +56,9 @@
<div class="mb-6"> <div class="mb-6">
<p class="dark:text-white font-medium text-lg"> <p class="dark:text-white font-medium text-lg">
{formatter?.format(calculatedCost) || calculatedCost} {formatter?.format(calculatedCost) || calculatedCost}
{#if isFree}
<span class="text-sm">(Free)</span>
{/if}
</p> </p>
{#if courseData?.metadata?.showDiscount} {#if courseData?.metadata?.showDiscount}
<p class="dark:text-white font-light text-sm text-gray-500"> <p class="dark:text-white font-light text-sm text-gray-500">
@@ -58,15 +73,9 @@
<!-- 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="Add to Cart" label="Join Course"
className="w-full sm:w-full py-3 mb-3" className="w-full sm:w-full py-3 mb-3"
onClick={addToCart} onClick={handleJoinCourse}
/>
<PrimaryButton
label="Buy Now"
className="w-full sm:w-full py-3 mb-3"
variant={VARIANTS.OUTLINED}
onClick={addToCart}
/> />
<p class="dark:text-white font-light text-sm text-gray-500">Early bird offer. Buy ASAP</p> <p class="dark:text-white font-light text-sm text-gray-500">Early bird offer. Buy ASAP</p>
</div> </div>
@@ -25,6 +25,7 @@
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
export let editMode = false;
export let courseData: Course = { export let courseData: Course = {
id: '', id: '',
title: '', title: '',
@@ -164,7 +165,11 @@
{/if} {/if}
</div> </div>
</header> </header>
<<<<<<< Updated upstream
<!-- <PricingSection className="md:hidden" {courseData} /> --> <!-- <PricingSection className="md:hidden" {courseData} /> -->
=======
<PricingSection className="md:hidden" {courseData} {editMode} />
>>>>>>> Stashed changes
<!-- Body --> <!-- Body -->
<div class="bg-white dark:bg-gray-800 w-full"> <div class="bg-white dark:bg-gray-800 w-full">
<div <div
+2 -2
View File
@@ -1,11 +1,11 @@
<script> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import IconButton from '$lib/components/IconButton/index.svelte'; import IconButton from '$lib/components/IconButton/index.svelte';
import PasswordEye from '../Icons/PasswordEye.svelte'; import PasswordEye from '../Icons/PasswordEye.svelte';
export let label = ''; export let label = '';
export let placeholder = ''; export let placeholder = '';
export let value = ''; export let value: string | number;
export let name = ''; export let name = '';
export let onKeyDown = () => {}; export let onKeyDown = () => {};
export let className = ''; export let className = '';
+2 -2
View File
@@ -4,10 +4,10 @@
export let id = ''; export let id = '';
export let value: string; export let value: string;
export let onChange = (html = '') => {}; export let onChange = (html: string) => {};
export let height = 300; export let height = 300;
export let placeholder = ''; export let placeholder = '';
export let editorWindowRef; export let editorWindowRef = null;
const apiKey = PUBLIC_TINYMCE_API_KEY; const apiKey = PUBLIC_TINYMCE_API_KEY;
+18
View File
@@ -0,0 +1,18 @@
import type { Course } from '../types';
export const isCourseFree = (cost: number) => !(Number(cost) > 0);
export const getStudentInviteLink = (_course: Course, orgSiteName: string) => {
const hash = encodeURIComponent(
btoa(
JSON.stringify({
groupId: _course.group?.id,
name: _course.title,
description: _course.description,
orgSiteName
})
)
);
return `https://${orgSiteName}.classroomio.com/invite/s/${hash}`;
};
@@ -1,7 +1,8 @@
export default (currency: string | undefined) => { export default (currency: string | undefined) => {
return new Intl.NumberFormat('en-US', { const locale = currency == 'NGN' ? 'en-NG' : 'en-US';
return new Intl.NumberFormat(locale, {
style: 'currency', style: 'currency',
currency: currency, currency: currency,
minimumFractionDigits: 2, minimumFractionDigits: 2
}); });
}; };
+1 -1
View File
@@ -3,7 +3,7 @@ import type { MailService } from '@sendgrid/mail';
import { SENDGRID_API_KEY } from '$env/static/private'; import { SENDGRID_API_KEY } from '$env/static/private';
export const SENDGRID_FROM = 'Rotimi Best <[email protected]>'; export const SENDGRID_FROM = 'Rotimi Best <[email protected]>';
export const SENDGRID_FROM_NOTIFY = 'ClassroomIO <notif[email protected]>'; export const SENDGRID_FROM_NOREPLY = 'ClassroomIO <norepl[email protected]>';
export let sendgrid: MailService; export let sendgrid: MailService;
+2 -1
View File
@@ -8,6 +8,7 @@ interface CourseMetadata {
videoUrl?: ''; videoUrl?: '';
showDiscount: false; showDiscount: false;
discount: 0; discount: 0;
paymentLink?: string;
reward?: { reward?: {
show: boolean; show: boolean;
description: string; description: string;
@@ -94,7 +95,7 @@ export interface Course {
logo?: string; logo?: string;
slug?: any; // type unknown; slug?: any; // type unknown;
metadata?: CourseMetadata; metadata?: CourseMetadata;
cost?: number; cost: number;
currency?: string; currency?: string;
group?: Group; group?: Group;
organization?: Organization; organization?: Organization;
@@ -1,7 +1,7 @@
import { render } from 'svelte-email'; import { render } from 'svelte-email';
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
import WelcomeTemplate from './template.svelte'; 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'; import { getSupabase } from '$lib/utils/functions/supabase';
const sendgrid = getSendgrid(); const sendgrid = getSendgrid();
@@ -30,7 +30,7 @@ export async function POST({ request }) {
} }
const options = { const options = {
from: SENDGRID_FROM_NOTIFY, from: SENDGRID_FROM_NOREPLY,
to, to,
subject: `[${orgName}] Welcome to Class`, subject: `[${orgName}] Welcome to Class`,
html: render({ html: render({
@@ -1,7 +1,7 @@
import { render } from 'svelte-email'; import { render } from 'svelte-email';
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
import WelcomeTemplate from './template.svelte'; 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'; import { getSupabase } from '$lib/utils/functions/supabase';
const sendgrid = getSendgrid(); const sendgrid = getSendgrid();
@@ -29,7 +29,7 @@ export async function POST({ request }) {
} }
const options = { const options = {
from: SENDGRID_FROM_NOTIFY, from: SENDGRID_FROM_NOREPLY,
to, to,
subject: `[${courseName}] You've got a new student!`, subject: `[${courseName}] You've got a new student!`,
html: render({ html: render({
@@ -1,7 +1,7 @@
import { render } from 'svelte-email'; import { render } from 'svelte-email';
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
import WelcomeTemplate from './template.svelte'; 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'; import { getSupabase } from '$lib/utils/functions/supabase';
const sendgrid = getSendgrid(); const sendgrid = getSendgrid();
@@ -32,7 +32,7 @@ export async function POST({ request }) {
const inviteLink = `${origin}/org/${orgSiteName}/courses`; const inviteLink = `${origin}/org/${orgSiteName}/courses`;
const options = { const options = {
from: SENDGRID_FROM_NOTIFY, from: SENDGRID_FROM_NOREPLY,
to, to,
subject: `You have been invited to a course in ${orgName}!`, subject: `You have been invited to a course in ${orgName}!`,
html: render({ html: render({
+2 -2
View File
@@ -1,7 +1,7 @@
import { render } from 'svelte-email'; import { render } from 'svelte-email';
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
import TeacherInviteTemplate from './template.svelte'; 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'; import { getSupabase } from '$lib/utils/functions/supabase';
const sendgrid = getSendgrid(); const sendgrid = getSendgrid();
@@ -47,7 +47,7 @@ export async function POST({ request }) {
console.log('inviteData', inviteData); console.log('inviteData', inviteData);
const options = { const options = {
from: SENDGRID_FROM_NOTIFY, from: SENDGRID_FROM_NOREPLY,
to: email, to: email,
subject: `Join ${name} on ClassroomIO`, subject: `Join ${name} on ClassroomIO`,
html: render({ html: render({
+7
View File
@@ -35,6 +35,13 @@
}); });
console.log('data', data); console.log('data', data);
if (error) throw error; 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) { } catch (error) {
submitError = error.error_description || error.message; submitError = error.error_description || error.message;
} finally { } finally {