Resume Logic
All checks were successful
ci / docker_image (push) Successful in 1m41s
ci / deploy (push) Successful in 17s

This commit is contained in:
Drake Marino 2025-03-28 11:16:15 -05:00
parent fc97a4271c
commit 77f3d182d4
16 changed files with 323 additions and 60 deletions

View File

@ -99,6 +99,15 @@ h1 {
'opsz' 20 'opsz' 20
} }
.icon-48 {
font-size: 48px !important;
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 20
}
input[type='search'], input[type='text'], input[type='password'], input[type='email'], input[type='tel'], input[type='number'], textarea, select { input[type='search'], input[type='text'], input[type='password'], input[type='email'], input[type='tel'], input[type='number'], textarea, select {
background-color: var(--bg-color); background-color: var(--bg-color);
color: var(--text-color); color: var(--text-color);
@ -429,3 +438,7 @@ h2 {
/*width: 100%;*/ /*width: 100%;*/
} }
/*.drop_zone {*/
/* border: 1px dashed var(--dull-primary-color);*/
/*}*/

View File

@ -11,6 +11,8 @@ import {
type Application type Application
} from '$lib/types'; } from '$lib/types';
import { sendEmployerNotificationEmail } from '$lib/emailer.server'; import { sendEmployerNotificationEmail } from '$lib/emailer.server';
import fs from 'fs';
import path from 'path';
export async function createUser(user: User): Promise<number> { export async function createUser(user: User): Promise<number> {
const password_hash: string = await bcrypt.hash(user.password!, 12); const password_hash: string = await bcrypt.hash(user.password!, 12);
@ -340,6 +342,7 @@ export async function getCompanyFullData(
), ),
user_data AS ( user_data AS (
SELECT SELECT
id,
username, username,
email, email,
phone, phone,
@ -491,7 +494,7 @@ export async function getCompanyEmployers(
last_signin AT TIME ZONE 'UTC' AS "lastSignIn", last_signin AT TIME ZONE 'UTC' AS "lastSignIn",
company_id as "companyId" company_id as "companyId"
FROM users FROM users
WHERE "company_code" = (SELECT company_code FROM companies WHERE id = ${id})) WHERE "company_id" = ${id})
SELECT SELECT
( (
SELECT row_to_json(company_data) SELECT row_to_json(company_data)
@ -530,6 +533,101 @@ export async function getCompanyEmployers(
}; };
} }
export async function getCompanyEmployersAndRequests(
id: number
): Promise<{ company: Company; employers: User[]; requests: User[] }> {
const data = await sql`
WITH company_data AS (
SELECT
id,
name,
description,
website,
created_at AT TIME ZONE 'UTC' AS "createdAt",
company_code AS "companyCode"
FROM companies
WHERE id = ${id}
),
employer_data AS (SELECT id,
username,
email,
phone,
full_name AS "fullName",
created_at AT TIME ZONE 'UTC' AS "createdAt",
last_signin AT TIME ZONE 'UTC' AS "lastSignIn",
company_id as "companyId"
FROM users
WHERE "company_id" = ${id}),
request_data AS (SELECT id,
username,
email,
phone,
full_name AS "fullName",
created_at AT TIME ZONE 'UTC' AS "createdAt",
last_signin AT TIME ZONE 'UTC' AS "lastSignIn",
company_id as "companyId"
FROM users
WHERE "company_code" = (SELECT company_code FROM companies WHERE id = ${id}))
SELECT
(
SELECT row_to_json(company_data)
FROM company_data
) AS company,
(
SELECT json_agg(row_to_json(employer_data))
FROM employer_data
) AS employers,
(
SELECT json_agg(row_to_json(request_data))
FROM request_data
) AS requests;
`;
if (!data) {
error(404, 'Company not found');
}
if (data[0].employers) {
data[0].employers.forEach(
(user: {
company: { id: any };
companyId: any;
createdAt: string | number | Date;
lastSignIn: string | number | Date;
}) => {
user.company = {
id: user.companyId
};
user.createdAt = new Date(user.createdAt);
user.lastSignIn = new Date(user.lastSignIn);
delete user.companyId;
}
);
}
if (data[0].requests) {
data[0].requests.forEach(
(user: {
company: { id: any };
companyId: any;
createdAt: string | number | Date;
lastSignIn: string | number | Date;
}) => {
user.company = {
id: user.companyId
};
user.createdAt = new Date(user.createdAt);
user.lastSignIn = new Date(user.lastSignIn);
delete user.companyId;
}
);
}
return {
company: <Company>data[0].company,
employers: <User[]>data[0].employers,
requests: <User[]>data[0].requests
};
}
export async function removeEmployerFromCompany(companyId: number, userId: number): Promise<void> { export async function removeEmployerFromCompany(companyId: number, userId: number): Promise<void> {
await sql` await sql`
UPDATE users UPDATE users
@ -748,7 +846,10 @@ export async function getApplications(postingId: number): Promise<Application[]>
username: application.username, username: application.username,
email: application.email, email: application.email,
phone: application.phone, phone: application.phone,
fullName: application.fullName fullName: application.fullName,
resume: fs.existsSync(
path.join(process.cwd(), 'static', 'uploads', 'resumes', `${application.userId}.pdf`)
)
}; };
delete application.userId; delete application.userId;
delete application.username; delete application.username;

View File

@ -12,6 +12,7 @@ export interface User {
company: Company | null; company: Company | null;
companyCode: string | null; companyCode: string | null;
companyId: number | null | undefined; companyId: number | null | undefined;
resume: boolean | null | undefined;
} }
export interface Company { export interface Company {

View File

@ -36,7 +36,7 @@
<link <link
rel="stylesheet" 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,open_in_new,person,search,sell,store,upload,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,cloud_upload,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"> <div class="flex min-h-screen flex-col">

View File

@ -18,5 +18,16 @@
We provide an accessible way for students to find internships and co-op opportunities, and for We provide an accessible way for students to find internships and co-op opportunities, and for
employers to find students to fill their positions. employers to find students to fill their positions.
</h2> </h2>
<p class="mt-16">
Create an account or sign in <a class="hyperlink-color hyperlink-underline" href="/signin"
>here</a
>.
</p>
<p>
Or head over to our postings page <a
class="hyperlink-color hyperlink-underline"
href="/postings">here</a
> to view all job opportunities.
</p>
</div> </div>
</div> </div>

View File

@ -1,15 +1,16 @@
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { deleteApplicationWithUser, getUserWithCompanyAndApplications } from '$lib/db/index.server'; import { deleteApplicationWithUser, getUserWithCompanyAndApplications } from '$lib/db/index.server';
import { getUserId } from '$lib/index.server'; import { getUserId } from '$lib/index.server';
import { type Actions, fail } from '@sveltejs/kit'; import { type Actions, fail, json, redirect } from '@sveltejs/kit';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { writeFileSync } from 'fs';
export const load: PageServerLoad = async ({ cookies }) => { export const load: PageServerLoad = async ({ cookies }) => {
const id = getUserId(cookies); const id = getUserId(cookies);
const userData = await getUserWithCompanyAndApplications(id); const userData = await getUserWithCompanyAndApplications(id);
const resumeExists = fs.existsSync( const resumeExists = fs.existsSync(
path.join(process.cwd(), 'static', 'uploads', 'resume', `${id}.pdf`) path.join(process.cwd(), 'static', 'uploads', 'resumes', `${id}.pdf`)
); );
return { return {
@ -19,12 +20,26 @@ export const load: PageServerLoad = async ({ cookies }) => {
}; };
export const actions: Actions = { export const actions: Actions = {
delete: async ({ url, cookies }) => { deleteApplication: async ({ url, cookies }) => {
const id = parseInt(url.searchParams.get('id')!); const id = parseInt(url.searchParams.get('id')!);
try { try {
await deleteApplicationWithUser(id, getUserId(cookies)); await deleteApplicationWithUser(id, getUserId(cookies));
} catch (err) { } catch (err) {
return fail(500, { errorMessage: `Internal Server Error: ${err}` }); return fail(500, { errorMessage: `Internal Server Error: ${err}` });
} }
},
uploadResume: async ({ request, cookies }) => {
const formData = await request.formData();
const file = formData.get('resume') as File;
if (!file) {
fail(400, { message: 'invalid' });
}
writeFileSync(
`static/uploads/resumes/${getUserId(cookies)}.pdf`,
Buffer.from(await file.arrayBuffer())
);
return { success: true };
} }
}; };

View File

@ -3,6 +3,8 @@
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let applicationToDelete: number = $state(0); let applicationToDelete: number = $state(0);
let { data, form }: PageProps = $props();
let resumeState = $state(data.resumeExists);
onMount(() => { onMount(() => {
if (!document.cookie.includes('jwt=')) { if (!document.cookie.includes('jwt=')) {
@ -42,8 +44,78 @@
document.getElementById('deleteConfirmModal')!.style.display = 'none'; document.getElementById('deleteConfirmModal')!.style.display = 'none';
} }
let resumeUpload; function openUpload() {
let { data, form }: PageProps = $props(); document.getElementById('uploadModal')!.style.display = 'block';
}
function closeUpload() {
document.getElementById('uploadModal')!.style.display = 'none';
}
let files: FileList | undefined | null = $state();
let file: File | undefined | null = $state();
$effect(() => {
if (files) {
if (files[files.length - 1]?.type == 'application/pdf') {
file = files[files.length - 1];
console.log(file.name);
}
}
});
function dropHandler(event: DragEvent) {
// Prevent default behavior (Prevent file from being opened)
event.preventDefault();
if (event.dataTransfer?.items) {
const items = event.dataTransfer.items;
// Use DataTransferItemList interface to access the file(s)
// If dropped items aren't files, reject them
if (items[0].kind === 'file') {
const upload = items[0].getAsFile();
if (upload?.type !== 'application/pdf') {
console.error('Invalid file type');
return;
}
file = upload;
}
}
}
function dragOverHandler(event: DragEvent) {
event.preventDefault();
}
async function handleSubmit(event: SubmitEvent) {
event.preventDefault(); // Prevent default form submission
if (!file) {
alert('Please select a file first');
return;
}
const formData = new FormData();
formData.append('resume', file); // Manually append the file
try {
const response = await fetch('?/uploadResume', {
method: 'POST',
body: formData
});
if (response.ok) {
const result = await response.json();
console.log('File uploaded successfully:', result);
closeUpload();
resumeState = true;
} else {
console.error('Upload failed');
}
} catch (error) {
console.error('Error uploading file:', error);
}
}
</script> </script>
<div class="base-container"> <div class="base-container">
@ -142,19 +214,18 @@
{/if} {/if}
<div> <div>
<div class="font-semibold">Résumé:</div> <div class="font-semibold">Résumé:</div>
{#if data.resumeExists} {#if resumeState}
<a class="pb-2" href="/uploads/resumes/{data.user.id}.pdf"> <a class="pb-2" href="/uploads/resumes/{data.user.id}.pdf">
<button class="dull-primary-bg-color rounded-md px-2.5 py-1" <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 ><span class="material-symbols-outlined align-middle">open_in_new</span> View résumé</button
> >
</a> </a>
<input type="file" accept=".pdf" /> <button class="mb-2 ml-1 rounded-md border px-2.5 py-1" onclick={openUpload}
<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 ><span class="material-symbols-outlined align-middle">upload</span> Upload new version</button
> >
{:else} {:else}
<!-- <div class="">No résumé submitted.</div>--> <div class="">No résumé submitted.</div>
<button class="dull-primary-bg-color mb-2 rounded-md px-2.5 py-1" <button class="dull-primary-bg-color mb-1 rounded-md px-2.5 py-1" onclick={openUpload}
><span class="material-symbols-outlined align-middle">upload</span> Upload</button ><span class="material-symbols-outlined align-middle">upload</span> Upload</button
> >
{/if} {/if}
@ -203,7 +274,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="mb-2 inline-flex w-full justify-between"> <div class="mb-2 inline-flex w-full justify-between">
<h2 class="font-semibold">Are you sure?</h2> <h2 class="font-semibold">Are you sure?</h2>
<button class="material-symbols-outlined" onclick={closeConfirm}>close</button> <button class="material-symbols-outlined" onclick={closeConfirm} type="button">close</button>
</div> </div>
<p>This will permanently delete this application. This action cannot be undone.</p> <p>This will permanently delete this application. This action cannot be undone.</p>
@ -211,7 +282,7 @@
<button <button
class="danger-bg-color rounded px-2 py-1" class="danger-bg-color rounded px-2 py-1"
type="submit" type="submit"
formaction="?/delete&id={applicationToDelete}">Delete application</button formaction="?/deleteApplication&id={applicationToDelete}">Delete application</button
> >
<button <button
class="separator-borders bg-color rounded px-2 py-1" class="separator-borders bg-color rounded px-2 py-1"
@ -221,3 +292,42 @@
</div> </div>
</div> </div>
</form> </form>
<form id="uploadModal" method="POST" class="modal" onsubmit={handleSubmit}>
<div class="modal-content">
<div class="mb-2 inline-flex w-full justify-between">
<h2 class="font-semibold">Résumé Upload</h2>
<button class="material-symbols-outlined" onclick={closeUpload} type="button">close</button>
</div>
<div>
<div
role="region"
class="dull-primary-border-color rounded-lg border-2 border-dashed"
ondrop={dropHandler}
ondragover={dragOverHandler}
>
<label for="resume" class="cursor-pointer p-4">
<div class="text-center">
<span class="material-symbols-outlined icon-48">cloud_upload</span>
<h3>Drag & drop your résumé here</h3>
<p class="">
or <span class="hyperlink-color hyperlink-underline">click here to browse.</span>
</p>
<!-- <label class="dull-primary-bg-color cursor-pointer rounded px-2 py-1" for="resume"-->
<!-- >Select a file</label-->
<!-- >-->
<input bind:files type="file" id="resume" accept=".pdf" class="hidden" />
</div>
</label>
</div>
<div class="mt-2">{file?.name}</div>
</div>
<div class="mt-4 flex justify-between">
<button class="dull-primary-bg-color rounded px-2 py-1" type="submit">Submit</button>
<button
class="separator-borders bg-color rounded px-2 py-1"
type="button"
onclick={closeUpload}>Cancel</button
>
</div>
</div>
</form>

View File

@ -174,7 +174,9 @@
<div class="modal-content"> <div class="modal-content">
<div class="mb-2 inline-flex w-full justify-between"> <div class="mb-2 inline-flex w-full justify-between">
<h2 class="font-semibold">Are you sure?</h2> <h2 class="font-semibold">Are you sure?</h2>
<button class="material-symbols-outlined" onclick={closeConfirm}>close</button> <button class="material-symbols-outlined" onclick={closeConfirm} type="button"
>close</button
>
</div> </div>
<p>This will permanently delete your account. This action cannot be undone.</p> <p>This will permanently delete your account. This action cannot be undone.</p>
<p>Please type "I understand" into the box below to confirm</p> <p>Please type "I understand" into the box below to confirm</p>

View File

@ -393,7 +393,9 @@
<div class="modal-content"> <div class="modal-content">
<div class="mb-2 inline-flex w-full justify-between"> <div class="mb-2 inline-flex w-full justify-between">
<h2 class="font-semibold">Are you sure?</h2> <h2 class="font-semibold">Are you sure?</h2>
<button class="material-symbols-outlined" onclick={closeConfirm}>close</button> <button class="material-symbols-outlined" onclick={closeConfirm} type="button"
>close</button
>
</div> </div>
<p> <p>
This will permanently delete user <span class="font-semibold">{data.user?.username}.</span This will permanently delete user <span class="font-semibold">{data.user?.username}.</span

View File

@ -25,7 +25,7 @@
<img <img
class="mb-2 inline-block h-32 rounded-lg" class="mb-2 inline-block h-32 rounded-lg"
src="/uploads/logos/{data.company.id}.jpg?timestamp=${Date.now()}" src="/uploads/logos/{data.company.id}.jpg?timestamp=${Date.now()}"
alt="User avatar" alt="Company Logo"
onerror={logoFallback} onerror={logoFallback}
height="128" height="128"
width="128" width="128"
@ -77,7 +77,7 @@
<div class="flex"> <div class="flex">
<img <img
class="mb-2 inline-block h-min rounded" class="mb-2 inline-block h-min rounded"
src="/uploads/avatars/{data.company.id}.svg?timestamp=${Date.now()}" src="/uploads/avatars/{user.id}.svg?timestamp=${Date.now()}"
alt="User avatar" alt="User avatar"
onerror={(e) => avatarFallback(e, user)} onerror={(e) => avatarFallback(e, user)}
height="32" height="32"
@ -85,7 +85,7 @@
/> />
<div class="pl-2"> <div class="pl-2">
<div class="pb-1 font-semibold"> <div class="pb-1 font-semibold">
{user.username}{user.fullName ? ` (${user.fullName})` : ''} {user.fullName}{`(${user.username})`}
</div> </div>
{#if user.email} {#if user.email}
<div class="pb-1"> <div class="pb-1">

View File

@ -93,7 +93,9 @@
<div class="modal-content"> <div class="modal-content">
<div class="mb-2 inline-flex w-full justify-between"> <div class="mb-2 inline-flex w-full justify-between">
<h2 class="font-semibold">Are you sure?</h2> <h2 class="font-semibold">Are you sure?</h2>
<button class="material-symbols-outlined" onclick={closeConfirm}>close</button> <button class="material-symbols-outlined" onclick={closeConfirm} type="button"
>close</button
>
</div> </div>
<p> <p>
This will permanently delete company <span class="font-semibold" This will permanently delete company <span class="font-semibold"

View File

@ -5,6 +5,7 @@ import {
editCompany, editCompany,
getCompany, getCompany,
getCompanyEmployers, getCompanyEmployers,
getCompanyEmployersAndRequests,
removeEmployerFromCompany removeEmployerFromCompany
} from '$lib/db/index.server'; } from '$lib/db/index.server';
import { PERMISSIONS } from '$lib/consts'; import { PERMISSIONS } from '$lib/consts';
@ -15,7 +16,7 @@ export const load: PageServerLoad = async ({ cookies, params }) => {
const id = parseInt(params.company); const id = parseInt(params.company);
const perms = getUserPerms(cookies); const perms = getUserPerms(cookies);
if (perms >= 0 && (perms & PERMISSIONS.MANAGE_COMPANIES) > 0) { if (perms >= 0 && (perms & PERMISSIONS.MANAGE_COMPANIES) > 0) {
return await getCompanyEmployers(id); return await getCompanyEmployersAndRequests(id);
} }
error(403, 'Unauthorized'); error(403, 'Unauthorized');
}; };

View File

@ -50,8 +50,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#if data.users !== undefined} {#if data.employers !== undefined}
{#each data.users as user} {#each data.employers as user}
{#if user.company?.id === data.id} {#if user.company?.id === data.id}
<tr class="h-8"> <tr class="h-8">
<td class="left">{user.id}</td> <td class="left">{user.id}</td>
@ -112,9 +112,7 @@
</div> </div>
</form> </form>
{/if} {/if}
{#if data.users && data.users.some((user) => { {#if data.requests !== null}
return user.company?.id !== data.id;
})}
<div class="content"> <div class="content">
<div class="elevated separator-borders m-4 rounded"> <div class="elevated separator-borders m-4 rounded">
<div class="bottom-border flex place-content-between"> <div class="bottom-border flex place-content-between">
@ -134,38 +132,36 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#if data.users !== undefined} {#each data.requests as user}
{#each data.users as user} {#if user.company?.id !== data.id}
{#if user.company?.id !== data.id} <tr class="h-8">
<tr class="h-8"> <td class="left">{user.id}</td>
<td class="left">{user.id}</td> <td>{user.username}</td>
<td>{user.username}</td> <td>{user.fullName || 'N/A'}</td>
<td>{user.fullName || 'N/A'}</td> <td>{user.email || 'N/A'}</td>
<td>{user.email || 'N/A'}</td> <td
<td >{user.createdAt?.toLocaleDateString('en-US', dateFormatOptions) ||
>{user.createdAt?.toLocaleDateString('en-US', dateFormatOptions) || 'unknown'}</td
'unknown'}</td >
> <td
<td >{user.lastSignIn?.toLocaleDateString('en-US', dateFormatOptions) ||
>{user.lastSignIn?.toLocaleDateString('en-US', dateFormatOptions) || 'unknown'}</td
'unknown'}</td >
> <td class="material-symbols-outlined"
<td class="material-symbols-outlined" ><form method="POST" class="flex">
><form method="POST" class="flex"> <button
<button class="hover-bg-color m-1 rounded text-green-600"
class="hover-bg-color m-1 rounded text-green-600" formaction="?/addEmployer&userId={user.id}">check</button
formaction="?/addEmployer&userId={user.id}">check</button >
> <button
<button class="hover-bg-color danger-color m-1 rounded"
class="hover-bg-color danger-color m-1 rounded" formaction="?/removeEmployer&userId={user.id}">close</button
formaction="?/removeEmployer&userId={user.id}">close</button >
> </form></td
</form></td >
> </tr>
</tr> {/if}
{/if} {/each}
{/each}
{/if}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -85,6 +85,15 @@
Applied: {application.createdAt.toLocaleDateString('en-US', dateFormatOptions)} Applied: {application.createdAt.toLocaleDateString('en-US', dateFormatOptions)}
</p> </p>
{/if} {/if}
{#if application.user?.resume}
<a
class="hyperlink-underline hyperlink-color"
href="/uploads/resumes/{application.user.id}.pdf"
target="_blank"
>
Résumé
</a>
{/if}
</div> </div>
<div class="left-border inline-block pl-4"> <div class="left-border inline-block pl-4">
<h2>Candidate Statement</h2> <h2>Candidate Statement</h2>

View File

@ -18,7 +18,7 @@
</script> </script>
<div class="signin-container place-items-center pt-8"> <div class="signin-container place-items-center pt-8">
<div class="elevated separator-borders bg content rounded-md p-8"> <div class="elevated separator-borders bg content mb-4 rounded-md p-8">
<h1 class="text-weight-semibold mb-4 text-center">Register</h1> <h1 class="text-weight-semibold mb-4 text-center">Register</h1>
<p>Create your account. Its free and only takes a minute!</p> <p>Create your account. Its free and only takes a minute!</p>
<form method="POST" class="arrange-vertically" use:enhance> <form method="POST" class="arrange-vertically" use:enhance>

View File

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB