final touches
This commit is contained in:
parent
fa14fe0496
commit
5eca1635f5
@ -318,11 +318,6 @@ h2 {
|
|||||||
@apply text-2xl
|
@apply text-2xl
|
||||||
}
|
}
|
||||||
|
|
||||||
/*.tooltip {*/
|
|
||||||
/* position: relative;*/
|
|
||||||
/* display: inline-block;*/
|
|
||||||
/*}*/
|
|
||||||
|
|
||||||
/* Tooltip text */
|
/* Tooltip text */
|
||||||
.tooltip-text {
|
.tooltip-text {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import sql from '$lib/db/db.server';
|
import sql from '$lib/db/db.server';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import { saveAvatar } from '$lib/index.server';
|
import { saveAvatar, saveLogo } from '$lib/index.server';
|
||||||
import {
|
import {
|
||||||
EmploymentType,
|
EmploymentType,
|
||||||
type User,
|
type User,
|
||||||
@ -281,6 +281,8 @@ export async function createCompany(company: Company): Promise<number> {
|
|||||||
RETURNING id;
|
RETURNING id;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
await saveLogo(company);
|
||||||
|
|
||||||
return response[0].id;
|
return response[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -616,7 +618,7 @@ export async function getPosting(id: number): Promise<Posting> {
|
|||||||
return posting;
|
return posting;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPostingFullData(id: number): Promise<Posting> {
|
export async function getPostingWithCompanyUser(id: number): Promise<Posting> {
|
||||||
const data = await sql`
|
const data = await sql`
|
||||||
WITH company_data AS (
|
WITH company_data AS (
|
||||||
SELECT
|
SELECT
|
||||||
@ -715,15 +717,36 @@ export async function deleteApplicationWithUser(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getApplications(postingId: number): Promise<Application[]> {
|
export async function getApplications(postingId: number): Promise<Application[]> {
|
||||||
const applications = await sql<Application[]>`
|
const data = await sql`
|
||||||
SELECT id, posting_id AS "postingId", user_id AS "userId", candidate_statement AS "candidateStatement", created_at AS "createdAt"
|
SELECT
|
||||||
FROM applications
|
a.id,
|
||||||
WHERE posting_id = ${postingId};
|
a.candidate_statement AS "candidateStatement",
|
||||||
|
a.created_at AS "createdAt",
|
||||||
|
u.id AS "userId",
|
||||||
|
u.username,
|
||||||
|
u.email,
|
||||||
|
u.phone,
|
||||||
|
u.full_name AS "fullName"
|
||||||
|
FROM applications a
|
||||||
|
JOIN users u ON a.user_id = u.id
|
||||||
|
WHERE a.posting_id = ${postingId};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
applications.forEach((application) => {
|
data.forEach((application) => {
|
||||||
application.createdAt = new Date(application.createdAt);
|
application.createdAt = new Date(application.createdAt);
|
||||||
|
application.user = {
|
||||||
|
id: application.userId,
|
||||||
|
username: application.username,
|
||||||
|
email: application.email,
|
||||||
|
phone: application.phone,
|
||||||
|
fullName: application.fullName
|
||||||
|
};
|
||||||
|
delete application.userId;
|
||||||
|
delete application.username;
|
||||||
|
delete application.email;
|
||||||
|
delete application.phone;
|
||||||
|
delete application.fullName;
|
||||||
});
|
});
|
||||||
|
|
||||||
return applications;
|
return <Application[]>(<unknown>data);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import path from 'path';
|
|||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import { type Cookies, error } from '@sveltejs/kit';
|
import { type Cookies, error } from '@sveltejs/kit';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import type { User } from '$lib/types';
|
import type { Company, User } from '$lib/types';
|
||||||
|
|
||||||
// TODO: Handle saving custom avatar uploads
|
// TODO: Handle saving custom avatar uploads
|
||||||
export async function saveAvatar(user: User): Promise<void> {
|
export async function saveAvatar(user: User): Promise<void> {
|
||||||
@ -14,6 +14,14 @@ export async function saveAvatar(user: User): Promise<void> {
|
|||||||
fs.writeFileSync(filePath, avatar);
|
fs.writeFileSync(filePath, avatar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function saveLogo(company: Company): Promise<void> {
|
||||||
|
const url = `https://ui-avatars.com/api/?background=random&format=svg&name=${encodeURIComponent(company.name!)}`;
|
||||||
|
const response = await fetch(url, { headers: { accept: 'image/svg+xml' } });
|
||||||
|
const avatar = await response.text();
|
||||||
|
const filePath = path.join('static', 'uploads', 'logos', `${company.id}.svg`);
|
||||||
|
fs.writeFileSync(filePath, avatar);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: change to return null instead of -1
|
// TODO: change to return null instead of -1
|
||||||
export function getUserPerms(cookies: Cookies): number {
|
export function getUserPerms(cookies: Cookies): number {
|
||||||
if (process.env.JWT_SECRET === undefined) {
|
if (process.env.JWT_SECRET === undefined) {
|
||||||
|
|||||||
@ -65,4 +65,5 @@ export interface Application {
|
|||||||
postingTitle: string | null;
|
postingTitle: string | null;
|
||||||
candidateStatement: string;
|
candidateStatement: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
user: User | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { getPosting, getPostingFullData } from '$lib/db/index.server';
|
import { getPosting, getPostingWithCompanyUser } from '$lib/db/index.server';
|
||||||
import { error, json } from '@sveltejs/kit';
|
import { error, json } from '@sveltejs/kit';
|
||||||
|
|
||||||
export async function GET({ url }) {
|
export async function GET({ url }) {
|
||||||
const id = url.searchParams.get('id');
|
const id = url.searchParams.get('id');
|
||||||
if (!id) return new Response(error(400, 'No id provided'));
|
if (!id) return new Response(error(400, 'No id provided'));
|
||||||
return json(await getPostingFullData(parseInt(id)));
|
return json(await getPostingWithCompanyUser(parseInt(id)));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,9 +12,9 @@
|
|||||||
day: 'numeric'
|
day: 'numeric'
|
||||||
};
|
};
|
||||||
|
|
||||||
function logoFallback(e: Event, posting: Posting) {
|
function logoFallback(e: Event, posting: Posting | undefined) {
|
||||||
(e.target as HTMLImageElement).src =
|
(e.target as HTMLImageElement).src =
|
||||||
`https://ui-avatars.com/api/?background=random&format=svg&name=${encodeURIComponent(posting.company.name || 'COMPANY')}`;
|
`https://ui-avatars.com/api/?background=random&format=svg&name=${encodeURIComponent(posting?.company.name || 'COMPANY')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchDetails(id: number) {
|
async function fetchDetails(id: number) {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { getPostingFullData, getPostings } from '$lib/db/index.server';
|
import { getPostingWithCompanyUser, getPostings } from '$lib/db/index.server';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params }) => {
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
return {
|
return {
|
||||||
posting: await getPostingFullData(parseInt(params.posting))
|
posting: await getPostingWithCompanyUser(parseInt(params.posting))
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,16 +1,20 @@
|
|||||||
import { type Actions, error, fail, redirect } from '@sveltejs/kit';
|
import { type Actions, error, fail, redirect } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { createApplication, getPostingFullData, getUserWithCompany } from '$lib/db/index.server';
|
import {
|
||||||
import { getUserId, getUserPerms } from '$lib/index.server';
|
createApplication,
|
||||||
|
getPostingWithCompanyUser,
|
||||||
|
getUserWithCompany
|
||||||
|
} from '$lib/db/index.server';
|
||||||
|
import { getUserCompanyId, getUserId, getUserPerms } from '$lib/index.server';
|
||||||
import { PERMISSIONS } from '$lib/consts';
|
import { PERMISSIONS } from '$lib/consts';
|
||||||
import type { Application } from '$lib/types';
|
import type { Application } from '$lib/types';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params, cookies }) => {
|
export const load: PageServerLoad = async ({ params, cookies }) => {
|
||||||
const id = parseInt(params.posting);
|
const id = parseInt(params.posting);
|
||||||
const perms = getUserPerms(cookies);
|
const perms = getUserPerms(cookies);
|
||||||
if (perms >= 0 && (perms & PERMISSIONS.MANAGE_USERS) > 0) {
|
if (perms >= 0 && (perms & PERMISSIONS.APPLY_FOR_JOBS) > 0) {
|
||||||
return {
|
return {
|
||||||
posting: await getPostingFullData(id)
|
posting: await getPostingWithCompanyUser(id)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
error(403, 'Unauthorized');
|
error(403, 'Unauthorized');
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
import { employmentTypeDisplayName, userState } from '$lib/shared.svelte';
|
import { employmentTypeDisplayName } from '$lib/shared.svelte';
|
||||||
import type { Posting } from '$lib/types';
|
import type { Posting } from '$lib/types';
|
||||||
import { PERMISSIONS } from '$lib/consts';
|
|
||||||
|
|
||||||
const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
|
|||||||
@ -0,0 +1,39 @@
|
|||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import { getUserCompanyId, getUserPerms } from '$lib/index.server';
|
||||||
|
import { PERMISSIONS } from '$lib/consts';
|
||||||
|
import {
|
||||||
|
deleteApplication,
|
||||||
|
getApplications,
|
||||||
|
getPostingWithCompanyUser
|
||||||
|
} from '$lib/db/index.server';
|
||||||
|
import { type Actions, error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params, cookies }) => {
|
||||||
|
const id = parseInt(params.posting);
|
||||||
|
const perms = getUserPerms(cookies);
|
||||||
|
if (
|
||||||
|
perms >= 0 &&
|
||||||
|
((perms & PERMISSIONS.MANAGE_POSTINGS) > 0 ||
|
||||||
|
((perms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && getUserCompanyId(cookies) === id))
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
applications: await getApplications(id)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
error(403, 'Unauthorized');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions: Actions = {
|
||||||
|
delete: async ({ url, cookies }) => {
|
||||||
|
const id = parseInt(url.searchParams.get('id')!);
|
||||||
|
const perms = getUserPerms(cookies);
|
||||||
|
if (
|
||||||
|
perms >= 0 &&
|
||||||
|
((perms & PERMISSIONS.MANAGE_POSTINGS) > 0 ||
|
||||||
|
((perms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && getUserCompanyId(cookies) === id))
|
||||||
|
) {
|
||||||
|
return await deleteApplication(id);
|
||||||
|
}
|
||||||
|
error(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,111 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
import type { PageProps } from './$types';
|
||||||
|
import { employmentTypeDisplayName } from '$lib/shared.svelte';
|
||||||
|
import type { Application, Posting } from '$lib/types';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
};
|
||||||
|
|
||||||
|
function logoFallback(e: Event, application: Application) {
|
||||||
|
(e.target as HTMLImageElement).src =
|
||||||
|
`https://ui-avatars.com/api/?background=random&format=svg&name=${encodeURIComponent(application.user.fullName || 'USER')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const acc = document.getElementsByClassName('accordion');
|
||||||
|
|
||||||
|
for (let i = 0; i < acc.length; i++) {
|
||||||
|
acc[i].addEventListener('click', function (this: HTMLElement, event: Event) {
|
||||||
|
this.classList.toggle('active');
|
||||||
|
this.children[1].innerHTML = this.classList.contains('active')
|
||||||
|
? 'arrow_drop_up'
|
||||||
|
: 'arrow_drop_down';
|
||||||
|
|
||||||
|
/* Toggle between hiding and showing the active panel */
|
||||||
|
let panel = this.nextElementSibling as HTMLElement;
|
||||||
|
if (panel.style.display === 'block') {
|
||||||
|
panel.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
panel.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let { data, form }: PageProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="elevated separator-borders m-4 rounded">
|
||||||
|
<div class="flex place-content-between">
|
||||||
|
<div class="p-3 font-semibold">
|
||||||
|
Application Management (Total: {data.applications?.length || 0})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{#each data.applications as application}
|
||||||
|
<button class="top-border accordion flex place-content-between">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
class="m-2 inline-block rounded"
|
||||||
|
src="/uploads/avatars/{application.user?.id || 'default'}.svg"
|
||||||
|
alt="Company Logo"
|
||||||
|
height="32"
|
||||||
|
width="32"
|
||||||
|
onerror={(e) => logoFallback(e, application)}
|
||||||
|
/>
|
||||||
|
<div class="inline-block py-1 text-left align-top">
|
||||||
|
<div class="font-semibold">
|
||||||
|
{application.user?.fullName}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{application.user?.username}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="material-symbols-outlined p-4">arrow_drop_down</div>
|
||||||
|
</button>
|
||||||
|
<div class="panel hidden p-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="inline-block">
|
||||||
|
<div class="inline-block pr-4 align-top">
|
||||||
|
{#if application.user?.email}
|
||||||
|
<p>Email: {application.user.email}</p>
|
||||||
|
{/if}
|
||||||
|
{#if application.user?.phone}
|
||||||
|
<p>Phone: {application.user.email}</p>
|
||||||
|
{/if}
|
||||||
|
{#if application.createdAt}
|
||||||
|
<p>
|
||||||
|
Applied: {application.createdAt.toLocaleDateString('en-US', dateFormatOptions)}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="left-border inline-block pl-4">
|
||||||
|
<h2>Candidate Statement</h2>
|
||||||
|
<h3 class="low-emphasis-text">
|
||||||
|
Why do you believe you are the best fit for this role?
|
||||||
|
</h3>
|
||||||
|
<p class="whitespace-pre-wrap">{application.candidateStatement}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="POST">
|
||||||
|
<button
|
||||||
|
class="material-symbols-outlined danger-color inline-block p-1 align-top"
|
||||||
|
type="submit"
|
||||||
|
formaction="?/delete&id={application.id}"
|
||||||
|
>
|
||||||
|
delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Loading…
Reference in New Issue
Block a user