final touches
This commit is contained in:
parent
fa14fe0496
commit
5eca1635f5
@ -318,11 +318,6 @@ h2 {
|
||||
@apply text-2xl
|
||||
}
|
||||
|
||||
/*.tooltip {*/
|
||||
/* position: relative;*/
|
||||
/* display: inline-block;*/
|
||||
/*}*/
|
||||
|
||||
/* Tooltip text */
|
||||
.tooltip-text {
|
||||
visibility: hidden;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import sql from '$lib/db/db.server';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { saveAvatar } from '$lib/index.server';
|
||||
import { saveAvatar, saveLogo } from '$lib/index.server';
|
||||
import {
|
||||
EmploymentType,
|
||||
type User,
|
||||
@ -281,6 +281,8 @@ export async function createCompany(company: Company): Promise<number> {
|
||||
RETURNING id;
|
||||
`;
|
||||
|
||||
await saveLogo(company);
|
||||
|
||||
return response[0].id;
|
||||
}
|
||||
|
||||
@ -616,7 +618,7 @@ export async function getPosting(id: number): Promise<Posting> {
|
||||
return posting;
|
||||
}
|
||||
|
||||
export async function getPostingFullData(id: number): Promise<Posting> {
|
||||
export async function getPostingWithCompanyUser(id: number): Promise<Posting> {
|
||||
const data = await sql`
|
||||
WITH company_data AS (
|
||||
SELECT
|
||||
@ -715,15 +717,36 @@ export async function deleteApplicationWithUser(
|
||||
}
|
||||
|
||||
export async function getApplications(postingId: number): Promise<Application[]> {
|
||||
const applications = await sql<Application[]>`
|
||||
SELECT id, posting_id AS "postingId", user_id AS "userId", candidate_statement AS "candidateStatement", created_at AS "createdAt"
|
||||
FROM applications
|
||||
WHERE posting_id = ${postingId};
|
||||
const data = await sql`
|
||||
SELECT
|
||||
a.id,
|
||||
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.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 { type Cookies, error } from '@sveltejs/kit';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import type { User } from '$lib/types';
|
||||
import type { Company, User } from '$lib/types';
|
||||
|
||||
// TODO: Handle saving custom avatar uploads
|
||||
export async function saveAvatar(user: User): Promise<void> {
|
||||
@ -14,6 +14,14 @@ export async function saveAvatar(user: User): Promise<void> {
|
||||
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
|
||||
export function getUserPerms(cookies: Cookies): number {
|
||||
if (process.env.JWT_SECRET === undefined) {
|
||||
|
||||
@ -65,4 +65,5 @@ export interface Application {
|
||||
postingTitle: string | null;
|
||||
candidateStatement: string;
|
||||
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';
|
||||
|
||||
export async function GET({ url }) {
|
||||
const id = url.searchParams.get('id');
|
||||
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'
|
||||
};
|
||||
|
||||
function logoFallback(e: Event, posting: Posting) {
|
||||
function logoFallback(e: Event, posting: Posting | undefined) {
|
||||
(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) {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
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 }) => {
|
||||
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 { PageServerLoad } from './$types';
|
||||
import { createApplication, getPostingFullData, getUserWithCompany } from '$lib/db/index.server';
|
||||
import { getUserId, getUserPerms } from '$lib/index.server';
|
||||
import {
|
||||
createApplication,
|
||||
getPostingWithCompanyUser,
|
||||
getUserWithCompany
|
||||
} from '$lib/db/index.server';
|
||||
import { getUserCompanyId, getUserId, getUserPerms } from '$lib/index.server';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import type { Application } from '$lib/types';
|
||||
|
||||
export const load: PageServerLoad = async ({ params, cookies }) => {
|
||||
const id = parseInt(params.posting);
|
||||
const perms = getUserPerms(cookies);
|
||||
if (perms >= 0 && (perms & PERMISSIONS.MANAGE_USERS) > 0) {
|
||||
if (perms >= 0 && (perms & PERMISSIONS.APPLY_FOR_JOBS) > 0) {
|
||||
return {
|
||||
posting: await getPostingFullData(id)
|
||||
posting: await getPostingWithCompanyUser(id)
|
||||
};
|
||||
}
|
||||
error(403, 'Unauthorized');
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
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 { PERMISSIONS } from '$lib/consts';
|
||||
|
||||
const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
||||
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