CLASSROOMIO-T-213: Replace all email notifications with Nodemailer
This commit is contained in:
@@ -5,12 +5,14 @@
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
||||
import NavIcons from './NavIcons.svelte';
|
||||
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
|
||||
|
||||
export let handleClick = () => {};
|
||||
export let label = '';
|
||||
export let isGroupActive = false;
|
||||
export let isExpanded = true;
|
||||
export let total = 0;
|
||||
export let isLoading = true;
|
||||
// export let subMenuItems = [];
|
||||
|
||||
function onClick() {
|
||||
@@ -23,32 +25,39 @@
|
||||
</script>
|
||||
|
||||
<div class="">
|
||||
<button
|
||||
class="item relative flex items-center py-3 px-4 ml-2 mb-1 {NavClasses.item} {isGroupActive &&
|
||||
NavClasses.active} w-[95%]"
|
||||
tabindex="0"
|
||||
on:click={onClick}
|
||||
>
|
||||
<NavIcons name={label} />
|
||||
<span class="font-bold text-md">{label}</span>
|
||||
{#if total}
|
||||
<span class="ml-1">({total})</span>
|
||||
{/if}
|
||||
<span class="grow" />
|
||||
|
||||
{#if label === 'Lessons'}
|
||||
<IconButton size="small" stopPropagation={true} onClick={toggleIsExpanded}>
|
||||
{#if isExpanded}
|
||||
<ChevronUpIcon class="carbon-icon dark:text-white" />
|
||||
{:else}
|
||||
<ChevronDownIcon class="carbon-icon dark:text-white" />
|
||||
{/if}
|
||||
</IconButton>
|
||||
{/if}
|
||||
</button>
|
||||
{#if isExpanded}
|
||||
<div in:slide out:slide class="flex flex-col">
|
||||
<slot />
|
||||
{#if isLoading}
|
||||
<div class="w-11/12 mx-auto">
|
||||
<SkeletonText class="py-3 px-4 mb-3 rounded-md" style="height: 40px;" />
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="item relative flex items-center py-3 px-4 ml-2 mb-1 {NavClasses.item} {isGroupActive &&
|
||||
NavClasses.active} w-[95%]"
|
||||
tabindex="0"
|
||||
on:click={onClick}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<NavIcons name={label} />
|
||||
<span class="font-bold text-md">{label}</span>
|
||||
{#if total}
|
||||
<span class="ml-1">({total})</span>
|
||||
{/if}
|
||||
<span class="grow" />
|
||||
|
||||
{#if label === 'Lessons' && !isLoading}
|
||||
<IconButton size="small" stopPropagation={true} onClick={toggleIsExpanded}>
|
||||
{#if isExpanded}
|
||||
<ChevronUpIcon class="carbon-icon dark:text-white" />
|
||||
{:else}
|
||||
<ChevronDownIcon class="carbon-icon dark:text-white" />
|
||||
{/if}
|
||||
</IconButton>
|
||||
{/if}
|
||||
</button>
|
||||
{#if isExpanded}
|
||||
<div in:slide out:slide class="flex flex-col">
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -255,6 +255,7 @@
|
||||
isGroupActive={(path || $page.url.pathname) === navItem.to}
|
||||
isExpanded={isLessonActive}
|
||||
total={navItem.isLecture ? ($lessons || []).length : 0}
|
||||
isLoading={!$course.id}
|
||||
>
|
||||
{#if navItem.isLecture}
|
||||
{#each $lessons as item, index}
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { getOrgTeam } from '$lib/utils/services/org';
|
||||
import type { OrgTeamMember } from '$lib/utils/types/org';
|
||||
import { sendTeacherCourseWelcome } from './utils';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { getStudentInviteLink } from '$lib/utils/functions/course';
|
||||
import {
|
||||
triggerSendEmail,
|
||||
NOTIFICATION_NAME
|
||||
} from '$lib/utils/services/notification/notification';
|
||||
|
||||
interface Tutor {
|
||||
id: number;
|
||||
@@ -52,7 +54,7 @@
|
||||
role_id: ROLE.TUTOR
|
||||
});
|
||||
|
||||
sendTeacherCourseWelcome({
|
||||
triggerSendEmail(NOTIFICATION_NAME.WELCOME_TEACHER_TO_COURSE, {
|
||||
to: tutor.email,
|
||||
name: tutor.text,
|
||||
orgName: $currentOrg.name,
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
|
||||
export async function sendTeacherCourseWelcome(params: {
|
||||
to: string;
|
||||
name: string;
|
||||
orgName: string;
|
||||
orgSiteName: string;
|
||||
courseName: string;
|
||||
}) {
|
||||
const { data } = await supabase.auth.getSession();
|
||||
const accessToken = data.session?.access_token || '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/email/course/teacher_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);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,11 @@
|
||||
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';
|
||||
import {
|
||||
triggerSendEmail,
|
||||
NOTIFICATION_NAME
|
||||
} from '$lib/utils/services/notification/notification';
|
||||
|
||||
export let open = false;
|
||||
export let paymentLink = '';
|
||||
@@ -41,7 +44,7 @@
|
||||
step = STEPS.STEP_2;
|
||||
|
||||
// Send email to first added teacher of student wanting to join
|
||||
sendTeacherStudentJoinRequest({
|
||||
triggerSendEmail(NOTIFICATION_NAME.SEND_TEACHER_STUDENT_BUY_REQUEST, {
|
||||
to: teacherEmail,
|
||||
courseName,
|
||||
studentEmail: fields.email,
|
||||
@@ -54,7 +57,7 @@
|
||||
|
||||
function onClickPaymentLink() {
|
||||
// Send email to student to send proof of payment to teacher
|
||||
sendStudentRequestReceipt({
|
||||
triggerSendEmail(NOTIFICATION_NAME.STUDENT_PROOVE_COURSE_PAYMENT, {
|
||||
to: fields.email,
|
||||
courseName,
|
||||
teacherEmail,
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import NotificationIcon from 'carbon-icons-svelte/lib/Notification.svelte';
|
||||
@@ -28,6 +28,10 @@
|
||||
localStorage.setItem('mode', $appStore.isDark ? 'dark' : '');
|
||||
}
|
||||
}
|
||||
|
||||
function shortenName(name: string) {
|
||||
return name?.substring(0, 2)?.toUpperCase() || '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="{navClass} flex w-full p-2 md:px-6 bg-primary-700 h-[48px]">
|
||||
@@ -58,7 +62,10 @@
|
||||
className="mr-2"
|
||||
/>
|
||||
{:else}
|
||||
<TextChip value={$currentOrg.shortName} className="bg-primary-200 font-bold" />
|
||||
<TextChip
|
||||
value={shortenName($currentOrg.name)}
|
||||
className="bg-primary-200 font-bold mr-2"
|
||||
/>
|
||||
{/if}
|
||||
<span class="line-clamp-1">
|
||||
{$currentOrg.name}
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import { sendInvite } from './utils';
|
||||
import {
|
||||
triggerSendEmail,
|
||||
NOTIFICATION_NAME
|
||||
} from '$lib/utils/services/notification/notification';
|
||||
|
||||
let emailsStr = '';
|
||||
let errorMessage = '';
|
||||
@@ -68,11 +71,13 @@
|
||||
]);
|
||||
}
|
||||
|
||||
// Send email invite
|
||||
sendInvite(email, {
|
||||
id: $currentOrg.id,
|
||||
name: $currentOrg.name,
|
||||
siteName: $currentOrg.siteName
|
||||
triggerSendEmail(NOTIFICATION_NAME.INVITE_TEACHER, {
|
||||
email: newMember.email,
|
||||
org: {
|
||||
id: $currentOrg.id,
|
||||
name: $currentOrg.name,
|
||||
siteName: $currentOrg.siteName
|
||||
}
|
||||
});
|
||||
|
||||
const isLast = index === emails.length - 1;
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
|
||||
export async function sendInvite(email: string, org: any) {
|
||||
const { data } = await supabase.auth.getSession();
|
||||
const accessToken = data.session?.access_token || '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/email/invite', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: accessToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
org
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
const result = await response.json();
|
||||
|
||||
console.log('Sent invite email', result);
|
||||
} catch (error) {
|
||||
console.error('Error sending invite email', error);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,24 @@
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
|
||||
export const NOTIFICATION_NAME = {
|
||||
WELCOME_TO_APP: 'WELCOME TO APP'
|
||||
WELCOME_TO_APP: 'WELCOME TO APP',
|
||||
INVITE_TEACHER: 'INVITE TEACHER',
|
||||
WELCOME_TEACHER_TO_COURSE: 'WELCOME TEACHER TO COURSE',
|
||||
SEND_TEACHER_STUDENT_BUY_REQUEST: 'SEND TEACHER STUDENT BUY REQUEST',
|
||||
STUDENT_PROOVE_COURSE_PAYMENT: 'STUDENT PROOVE COURSE PAYMENT',
|
||||
STUDENT_COURSE_WELCOME: 'STUDENT COURSE WELCOME',
|
||||
TEACHER_STUDENT_JOINED: 'TEACHER STUDENT JOINED'
|
||||
};
|
||||
|
||||
const NAME_TO_PATH = {
|
||||
[NOTIFICATION_NAME.WELCOME_TO_APP]: '/api/email/welcome'
|
||||
[NOTIFICATION_NAME.WELCOME_TO_APP]: '/api/email/welcome',
|
||||
[NOTIFICATION_NAME.INVITE_TEACHER]: '/api/email/invite',
|
||||
[NOTIFICATION_NAME.WELCOME_TEACHER_TO_COURSE]: '/api/email/course/teacher_welcome',
|
||||
[NOTIFICATION_NAME.SEND_TEACHER_STUDENT_BUY_REQUEST]:
|
||||
'/api/email/course/teacher_student_buycourse',
|
||||
[NOTIFICATION_NAME.STUDENT_PROOVE_COURSE_PAYMENT]: '/api/email/course/student_prove_payment',
|
||||
[NOTIFICATION_NAME.STUDENT_COURSE_WELCOME]: '/api/email/course/student_welcome',
|
||||
[NOTIFICATION_NAME.TEACHER_STUDENT_JOINED]: '/api/email/course/teacher_student_joined'
|
||||
};
|
||||
|
||||
const getAccessToken = async () => {
|
||||
@@ -14,7 +27,7 @@ const getAccessToken = async () => {
|
||||
return data.session?.access_token || '';
|
||||
};
|
||||
|
||||
export const triggerSendEmail = async (name: string, body: { [k: string]: string }) => {
|
||||
export const triggerSendEmail = async (name: string, body: { [k: string]: any }) => {
|
||||
try {
|
||||
const accessToken = await getAccessToken();
|
||||
const path = NAME_TO_PATH[name];
|
||||
@@ -37,8 +50,8 @@ export const triggerSendEmail = async (name: string, body: { [k: string]: string
|
||||
}
|
||||
const result = await response.json();
|
||||
|
||||
console.log(`${name} sent email result`, result);
|
||||
console.log(name, result);
|
||||
} catch (error) {
|
||||
console.log(`${name} sent email error`, error);
|
||||
console.error(name, error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import { withEmailTemplate } from './template';
|
||||
import { SMTP_HOST, SMTP_USER, SMTP_PASSWORD } from '$env/static/private';
|
||||
import {
|
||||
SMTP_HOST,
|
||||
SMTP_USER,
|
||||
SMTP_PASSWORD,
|
||||
SMTP_USER_HELP,
|
||||
SMTP_PASSWORD_HELP
|
||||
} from '$env/static/private';
|
||||
|
||||
const content = `
|
||||
<div>
|
||||
@@ -13,18 +19,20 @@ Your ClassroomIO Team
|
||||
</div>
|
||||
`;
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: 465,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: SMTP_USER,
|
||||
pass: SMTP_PASSWORD
|
||||
}
|
||||
});
|
||||
let transporter: nodemailer.Transporter;
|
||||
|
||||
const getTransporter = async (): Promise<boolean> => {
|
||||
const getTransporter = async (isPersonal?: boolean): Promise<boolean> => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
transporter = nodemailer.createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: 465,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: isPersonal ? SMTP_USER : SMTP_USER_HELP,
|
||||
pass: isPersonal ? SMTP_PASSWORD : SMTP_PASSWORD_HELP
|
||||
}
|
||||
});
|
||||
|
||||
transporter.verify(function (error, success) {
|
||||
if (error) {
|
||||
console.log('Transporter error', error);
|
||||
@@ -38,19 +46,26 @@ const getTransporter = async (): Promise<boolean> => {
|
||||
};
|
||||
|
||||
export const sendEmail = async ({
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
content
|
||||
content,
|
||||
isPersonalEmail,
|
||||
replyTo
|
||||
}: {
|
||||
from?: string;
|
||||
to: string;
|
||||
subject: string;
|
||||
content: string;
|
||||
isPersonalEmail?: boolean;
|
||||
replyTo?: string;
|
||||
}) => {
|
||||
await getTransporter();
|
||||
await getTransporter(isPersonalEmail);
|
||||
const info = await transporter.sendMail({
|
||||
from: '"Best from ClassroomIO" <[email protected]>', // sender address
|
||||
from: from || '"Best from ClassroomIO" <[email protected]>', // sender address
|
||||
to, // list of receivers
|
||||
subject, // Subject line
|
||||
replyTo,
|
||||
html: withEmailTemplate(content) // html body
|
||||
});
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ export const withEmailTemplate = (content: string) =>
|
||||
|
||||
.button {
|
||||
margin-top:12px;
|
||||
background: #0f172a;
|
||||
background: #0233BD;
|
||||
border-radius: 8px;
|
||||
text-decoration: none !important;
|
||||
color: #fff !important;
|
||||
@@ -110,10 +110,10 @@ export const withEmailTemplate = (content: string) =>
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00c4b8;
|
||||
color: #0233BD;
|
||||
}
|
||||
a:hover {
|
||||
color: #00e6ca;
|
||||
color: #0233BD;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
|
||||
Reference in New Issue
Block a user