emailer and resume start
ci / docker_image (push) Successful in 1m34s
ci / deploy (push) Successful in 16s

This commit is contained in:
2025-03-23 12:26:12 -05:00
parent 4f9e770270
commit 1877aee8a5
11 changed files with 292 additions and 167 deletions
+4
View File
@@ -240,6 +240,10 @@ th.left, td.left {
color: var(--dull-primary-color);
}
.dull-primary-border-color {
border-color: var(--dull-primary-color);
}
.primary-text-color {
color: var(--primary-color);
}
+33
View File
@@ -10,6 +10,7 @@ import {
type Posting,
type Application
} from '$lib/types';
import { sendEmployerNotificationEmail } from '$lib/emailer.server';
export async function createUser(user: User): Promise<number> {
const password_hash: string = await bcrypt.hash(user.password!, 12);
@@ -700,6 +701,9 @@ export async function createApplication(application: Application): Promise<numbe
RETURNING id;
`;
sendEmployerNotificationEmail(application.postingId).catch((err) => {
console.error('Failed to send employer notification email: ', err);
});
return response[0].id;
}
@@ -764,3 +768,32 @@ export async function setUserCompanyId(userId: number, companyId: number): Promi
WHERE id = ${userId};
`;
}
export async function getNotificationInfo(
postingId: number
): Promise<{ title: string; emails: string[] }> {
const data = await sql`
WITH posting_data AS (
SELECT title, company_id
FROM postings
WHERE id = ${postingId}
),
user_emails AS (
SELECT email
FROM users
WHERE company_id = (SELECT company_id FROM posting_data)
)
SELECT
(SELECT title FROM posting_data) AS title,
(SELECT json_agg(email) FROM user_emails) AS emails;
`;
if (!data || !data[0]) {
error(404, 'Posting not found');
}
return {
title: data[0].title,
emails: data[0].emails
};
}
+26
View File
@@ -0,0 +1,26 @@
// Import the Nodemailer library
import nodemailer from 'nodemailer';
import { getNotificationInfo } from '$lib/db/index.server';
// Create a transporter object using SMTP transport
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: Number(process.env.EMAIL_PORT),
secure: true, // true for 465, false for other ports
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
});
export async function sendEmployerNotificationEmail(postingId: number) {
let info = await getNotificationInfo(postingId);
// Send mail with defined transport object
await transporter.sendMail({
from: `CareerConnect Notifications <${process.env.EMAIL_USER}>`,
// to: info.emails.join(', '), // EMAILING OF REAL COMPANIES DISABLED, UNCOMMENT TO ENABLE
to: 'drake@marinodev.com', // TEMPORARY EMAIL FOR TESTING
subject: 'New Application Received!',
text: `A new application has been received for the posting '${info.title}'!\n\nCheck it out at ${process.env.BASE_URL}/postings/${postingId}/manage/applications`
});
}
+1 -1
View File
@@ -36,7 +36,7 @@
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..40,400,0,0&display=block&icon_names=account_circle,arrow_drop_down,arrow_drop_up,calendar_today,call,check,close,dark_mode,delete,description,edit,group,info,light_mode,login,mail,person,search,sell,store,visibility,visibility_off,work"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..40,400,0,0&display=block&icon_names=account_circle,arrow_drop_down,arrow_drop_up,calendar_today,call,check,close,dark_mode,delete,description,edit,group,info,light_mode,login,mail,open_in_new,person,search,sell,store,upload,visibility,visibility_off,work"
/>
<div class="flex min-h-screen flex-col">
+11 -1
View File
@@ -2,10 +2,20 @@ import type { PageServerLoad } from './$types';
import { deleteApplicationWithUser, getUserWithCompanyAndApplications } from '$lib/db/index.server';
import { getUserId } from '$lib/index.server';
import { type Actions, fail } from '@sveltejs/kit';
import fs from 'fs';
import path from 'path';
export const load: PageServerLoad = async ({ cookies }) => {
const id = getUserId(cookies);
return await getUserWithCompanyAndApplications(id);
const userData = await getUserWithCompanyAndApplications(id);
const resumeExists = fs.existsSync(
path.join(process.cwd(), 'static', 'uploads', 'resume', `${id}.pdf`)
);
return {
...userData,
resumeExists
};
};
export const actions: Actions = {
+27 -1
View File
@@ -8,6 +8,9 @@
if (!document.cookie.includes('jwt=')) {
window.location.href = '/signin';
}
if (window.location.search.includes('refresh')) {
location.replace(location.pathname);
}
});
const dateFormatOptions: Intl.DateTimeFormatOptions = {
@@ -39,6 +42,7 @@
document.getElementById('deleteConfirmModal')!.style.display = 'none';
}
let resumeUpload;
let { data, form }: PageProps = $props();
</script>
@@ -136,6 +140,25 @@
</div>
</div>
{/if}
<div>
<div class="font-semibold">Résumé:</div>
{#if data.resumeExists}
<a class="pb-2" href="/uploads/resumes/{data.user.id}.pdf">
<button class="dull-primary-bg-color rounded-md px-2.5 py-1"
><span class="material-symbols-outlined align-middle">open_in_new</span> View résumé</button
>
</a>
<input type="file" accept=".pdf" />
<button class="ml-2 rounded-md border px-2.5 py-1"
><span class="material-symbols-outlined align-middle">upload</span> Upload new version</button
>
{:else}
<!-- <div class="">No résumé submitted.</div>-->
<button class="dull-primary-bg-color mb-2 rounded-md px-2.5 py-1"
><span class="material-symbols-outlined align-middle">upload</span> Upload</button
>
{/if}
</div>
<div class="top-border pt-2 font-semibold">
Permissions: <span class="font-normal">{data.user.perms}</span>
</div>
@@ -148,7 +171,10 @@
<div class="top-border flex justify-between p-3">
<div class="inline-block">
<div class="font-semibold">
Applied to: <span class="font-normal">{application.postingTitle}</span>
Applied to: <a
class="hover-hyperlink font-normal"
href="/postings/{application.postingId}">{application.postingTitle}</a
>
</div>
<div class="font-semibold">
Applied on: <span class="font-normal"
+2
View File
@@ -122,6 +122,8 @@
class="dull-primary-bg-color inline-block h-min rounded-md px-2.5 py-1 align-top"
href="/postings/{details.id}/apply">Apply</a
>
{:else}
Sign-in to apply
{/if}
</div>
<div class="scrollbar-on-elevated details-height overflow-y-scroll">
@@ -97,8 +97,9 @@
></textarea>
</div>
<p>
Your account information will be submitted along with this application. If there is any
other information you would like the employer to know, please add it in the box above.
Your account information and résumé (if supplied) will be submitted along with this
application. If there is any other information you would like the employer to know, please
add it in the box above.
</p>
{#if form?.errorMessage}
+1 -1
View File
@@ -30,7 +30,7 @@ export const actions: Actions = {
setJWT(cookies, user);
await updateLastSignin(username);
// redirect to home page
throw redirect(303, '/account');
throw redirect(303, '/account?refresh=true');
} else {
return fail(400, { errorMessage: 'Missing username or password' });
}