chore: update to so many things

This commit is contained in:
Andre Henriques 2024-10-13 11:37:19 +01:00
parent 5253204e17
commit 5380eaffeb
10 changed files with 410 additions and 145 deletions

View File

@ -1,11 +1,12 @@
package com.andr3h3nriqu3s.applications
import java.sql.ResultSet
import java.util.UUID
import java.util.Date
import java.text.SimpleDateFormat
import java.util.Date
import java.util.UUID
import kotlin.collections.emptyList
import kotlin.collections.setOf
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.RowMapper
@ -20,6 +21,7 @@ import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
data class Application(
var id: String,
@ -37,6 +39,7 @@ data class Application(
var linked_application: String,
var status_history: String,
var application_time: String,
var create_time: String,
var flairs: List<Flair>,
var views: List<View>,
) {
@ -58,6 +61,7 @@ data class Application(
rs.getString("linked_application"),
rs.getString("status_history"),
rs.getString("application_time"),
rs.getString("create_time"),
emptyList(),
emptyList(),
)
@ -109,6 +113,77 @@ class ApplicationsController(
return CVData(application.company, application.recruiter, application.message, flairs)
}
/** Create a new application from the link */
@PostMapping(path = ["/link"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun submitLink(
@RequestBody submit: SubmitRequest,
@RequestHeader("token") token: String
): Application {
val user = sessionService.verifyTokenThrow(token)
var application =
Application(
UUID.randomUUID().toString(),
submit.text,
submit.text,
submit.text,
"New Application",
user.id,
"",
"",
0,
"",
"",
"",
"",
"",
"",
"",
emptyList(),
emptyList(),
)
if (!applicationService.createApplication(user, application)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Application already exists", null)
}
print("Created: ")
println(application)
return application
}
@PostMapping(path = ["/text/flair"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun textFlair(
@RequestBody info: FlairRequest,
@RequestHeader("token") token: String
): Int {
val user = sessionService.verifyTokenThrow(token)
val application = applicationService.findApplicationById(user, info.id)
if (application == null) {
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Application not found", null)
}
val flairs = flairService.listUser(user)
var count = 0
for (flair: Flair in flairs) {
val regex =
Regex(
".*" + flair.expr + ".*",
setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL)
)
if (regex.matches(info.text)) {
count += 1
flairService.linkFlair(application, flair)
}
}
return count
}
@PostMapping(path = ["/text"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun submitText(
@RequestBody submit: SubmitRequest,
@ -179,7 +254,7 @@ class ApplicationsController(
if (elm.contains("linkedin")) elm.split("?")[0] else elm,
if (elm.contains("linkedin")) elm.split("?")[0] else null,
if (elm.contains("linkedin")) elm.split("?")[0] else null,
"New Aplication",
"New Application",
user.id,
"",
"",
@ -190,6 +265,7 @@ class ApplicationsController(
"",
"",
"",
"",
emptyList(),
emptyList(),
)
@ -205,37 +281,6 @@ class ApplicationsController(
return applications.size
}
@PostMapping(path = ["/text/flair"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun textFlair(
@RequestBody info: FlairRequest,
@RequestHeader("token") token: String
): Int {
val user = sessionService.verifyTokenThrow(token)
val application = applicationService.findApplicationById(user, info.id)
if (application == null) {
throw NotFound()
}
val flairs = flairService.listUser(user)
var count = 0
for (flair: Flair in flairs) {
val regex =
Regex(
".*" + flair.expr + ".*",
setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL)
)
if (regex.matches(info.text)) {
count += 1
flairService.linkFlair(application, flair)
}
}
return count
}
@PostMapping(path = ["/list"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun list(
@RequestBody info: ListRequest,
@ -267,10 +312,10 @@ class ApplicationsController(
}
application.status = info.status
val status_string = "${info.status}";
val status_string = "${info.status}"
var status_history = application.status_history.split(",").filter { it.length >= 1 }
if (status_history.indexOf(status_string) == -1) {
status_history = status_history.plus("${info.status}");
status_history = status_history.plus("${info.status}")
}
application.status_history = status_history.joinToString(",") { it }
@ -308,11 +353,11 @@ class ApplicationsController(
var application = applicationService.findApplicationById(user, info.id)
if (application == null) {
throw NotFound()
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Application not found", null)
}
if (application.unique_url != null) {
throw BadRequest()
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Application already has unique_url", null)
}
application.original_url = application.url
@ -341,6 +386,7 @@ class ApplicationsController(
applicationService.update(application)
application.flairs = flairService.listFromLinkApplicationId(application.id)
application.views = viewService.listFromApplicationId(application.id)
return application
}
@ -485,6 +531,7 @@ class ApplicationService(
return false
}
// Create time is auto created by the database
db.update(
"insert into applications (id, url, original_url, unique_url, title, user_id, extra_data, payrange, status, company, recruiter, message, linked_application, status_history, application_time) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
application.id,
@ -527,7 +574,6 @@ class ApplicationService(
.toList()
}
return db.query(
"select * from applications where user_id=? and status=? order by title asc;",
arrayOf(user.id, info.status),
@ -537,6 +583,7 @@ class ApplicationService(
}
public fun update(application: Application): Application {
// I don't want ot update create_time
db.update(
"update applications set url=?, original_url=?, unique_url=?, title=?, user_id=?, extra_data=?, payrange=?, status=?, company=?, recruiter=?, message=?, linked_application=?, status_history=?, application_time=? where id=?",
application.url,

View File

@ -25,7 +25,8 @@ create table if not exists applications (
extra_data text,
status integer,
linked_application text default '',
application_time text default ''
application_time text default '',
create_time timestamp default now()
);
create table if not exists views (

View File

@ -40,3 +40,5 @@ ## Building
## TODO
https://www.glassdoor.co.uk/job-listing/junior-software-developer-full-stack-onnec-group-JV_IC2671300_KO0,36_KE37,48.htm?jl=1009478590946&utm_source=jobsForYou&utm_medium=email&utm_content=jobs-for-you-jobsForYou-jobpos5-1009478590946&utm_campaign=jobsForYou&src=GD_JOB_AD&uido=5EF1E454F911F36A51BB1DD9CB97C8DB&ao=1136043&jrtk=6-y100011i9mkhgisgqqb801ab7157ddf833b2e1c&cs=1_57b69a24&s=362&t=REC_JOBS&pos=105&guid=00000192656d7c18b164f224f0a88e83&jobListingId=1009478590946&ea=1&vt=e&cb=1728410338190&ctt=1728466768707
https://careers.veeva.com/job/866d4776-9d23-4311-ab16-4ebff725984d/frontend-engineer-react-remote-london-united-kingdom/

View File

@ -49,6 +49,8 @@ export type Application = {
message: string;
linked_application: string;
application_time: string;
create_time: string;
status_history: string;
flairs: Flair[];
views: View[];
};

View File

@ -150,7 +150,7 @@
<div class="p-3 bg-white w-[190mm] rounded-lg">
<h1 class="flex gap-5 items-end">
Your Ad & My skills {#if flairs.length > 0}<input
placeholder="Loking for other skills search?"
placeholder="Search other skills!"
class="flex-grow text-blue-500 print:hidden"
bind:value={otherSearch}
/>

View File

@ -21,119 +21,125 @@
| AsEnum<typeof ApplicationStatus>
| 'Linkedin'
| 'Glassdoor'
| 'Unknown Source'
| 'Applications';
| 'Direct Source';
let nodeTypes: Record<NodeType, number> = {
[ApplicationStatus.ToApply]: 0,
[ApplicationStatus.WorkingOnIt]: 0,
[ApplicationStatus.Ignore]: 0,
[ApplicationStatus.ApplyedButSaidNo]: 0,
[ApplicationStatus.Expired]: 0,
[ApplicationStatus.Applyed]: 0,
[ApplicationStatus.TasksToDo]: 0,
[ApplicationStatus.LinkedApplication]: 0,
Linkedin: 0,
Glassdoor: 0,
'Unknown Source': 0,
Applications: applications.length
};
let graph = {} as Record<string, Record<string, number>>;
const showPercentage: string[] = [
`${ApplicationStatus.ToApply}`,
`${ApplicationStatus.Ignore}`,
`${ApplicationStatus.Expired}`,
`${ApplicationStatus.Applyed}`,
`${ApplicationStatus.LinkedApplication}`,
`${ApplicationStatus.ApplyedButSaidNo}`,
`${ApplicationStatus.TasksToDo}`
];
const baseLinks: { source: NodeType; target: NodeType; value: 0 | 1; end?: boolean }[] =
[
{ source: 'Linkedin', target: 'Applications', value: 0 },
{ source: 'Glassdoor', target: 'Applications', value: 0 },
{ source: 'Unknown Source', target: 'Applications', value: 0 },
{ source: 'Applications', target: ApplicationStatus.ToApply, value: 1 },
{ source: 'Applications', target: ApplicationStatus.Ignore, value: 1 },
{ source: 'Applications', target: ApplicationStatus.Expired, value: 1 },
{ source: 'Applications', target: ApplicationStatus.Applyed, value: 1 },
{
source: 'Applications',
target: ApplicationStatus.LinkedApplication,
value: 1
},
{
source: ApplicationStatus.Applyed,
target: ApplicationStatus.ApplyedButSaidNo,
value: 1
},
{
source: ApplicationStatus.Applyed,
target: ApplicationStatus.TasksToDo,
value: 1
function addGraph(inSource: NodeType, inTarget: NodeType) {
const source = `${inSource}`;
const target = `${inTarget}`;
if (graph[source] == undefined) {
graph[source] = {} as Record<NodeType, number>;
}
graph[source][target] = (graph[source][target] ?? 0) + 1;
return target as NodeType;
}
];
applications.forEach((a) => {
let source: NodeType;
if (a.url.includes('linkedin')) {
nodeTypes['Linkedin'] += 1;
source = 'Linkedin';
} else if (a.url.includes('glassdoor')) {
nodeTypes['Glassdoor'] += 1;
source = 'Glassdoor';
} else {
nodeTypes['Unknown Source'] += 1;
}
if (a.status !== ApplicationStatus.WorkingOnIt) {
nodeTypes[a.status] += 1;
} else {
nodeTypes[ApplicationStatus.ToApply] += 1;
source = 'Direct Source';
}
if (
[ApplicationStatus.ApplyedButSaidNo, ApplicationStatus.TasksToDo].includes(
(
[
ApplicationStatus.Ignore,
ApplicationStatus.Expired,
ApplicationStatus.LinkedApplication,
ApplicationStatus.ToApply,
ApplicationStatus.Applyed
] as AsEnum<typeof ApplicationStatus>[]
).includes(a.status)
) {
addGraph(source, a.status);
return;
}
// Edge case for working on it
if (a.status === ApplicationStatus.WorkingOnIt) {
addGraph(source, ApplicationStatus.ToApply);
return;
}
if (a.status == ApplicationStatus.ApplyedButSaidNo) {
const history = a.status_history.split(',');
source = addGraph(source, ApplicationStatus.Applyed);
if (history.includes(`${ApplicationStatus.TasksToDo}`)) {
source = addGraph(source, ApplicationStatus.TasksToDo);
}
addGraph(source, ApplicationStatus.ApplyedButSaidNo);
} else if (
([ApplicationStatus.TasksToDo] as AsEnum<typeof ApplicationStatus>[]).includes(
a.status
)
) {
nodeTypes[ApplicationStatus.Applyed] += 1;
addGraph(source, ApplicationStatus.Applyed);
addGraph(ApplicationStatus.Applyed, a.status);
}
});
let inNodes: string[] = [];
let nodes = (Object.keys(nodeTypes) as (keyof typeof nodeTypes)[])
.filter((a) => nodeTypes[a] > 0)
.map((a, i) => {
inNodes.push(`${a}`);
const base = {
value: nodeTypes[a],
originalValue: a,
id: '',
index: i,
percentage: Math.trunc((nodeTypes[a] / applications.length) * 100),
end: showPercentage.includes(`${a}`)
};
if (Number.isNaN(Number(a))) {
base.id = a as string;
} else {
base.id = ApplicationStatusMaping[a as AsEnum<typeof ApplicationStatus>];
let inNodes: string[] = Object.keys(graph).reduce((acc, elm) => {
const arr = Object.keys(graph[elm]).concat(elm);
for (const i of arr) {
if (!acc.includes(i)) {
acc.push(i);
}
}
return acc;
}, [] as string[]);
function getGraphValueFor(node: string): number {
return Object.keys(graph).reduce((acc, i) => {
if (i == node) return acc;
if (graph[i][node] != undefined) {
return acc + graph[i][node];
}
return acc;
}, 0);
}
let nodes = inNodes.map((node, i) => {
let name = '';
if (Number.isNaN(Number(node))) {
name = node;
} else {
name = ApplicationStatusMaping[Number(node) as AsEnum<typeof ApplicationStatus>];
}
const value = getGraphValueFor(node);
const base = {
value: value,
originalValue: node,
id: name,
index: i,
percentage: Math.trunc((value / applications.length) * 100),
end: true
};
return base;
});
const links = baseLinks
.filter(
(link) =>
inNodes.includes(`${link.source}`) && inNodes.includes(`${link.target}`)
)
.map((link) => {
const source = inNodes.indexOf(`${link.source}`);
const target = inNodes.indexOf(`${link.target}`);
type Link = {
source: number;
target: number;
value: number;
}
const links = Object.keys(graph).reduce((acc, source) => {
return acc.concat(Object.keys(graph[source]).map((target) => {
const ns = inNodes.indexOf(`${source}`);
const nt = inNodes.indexOf(`${target}`);
return {
source: source,
target: target,
value: [nodes[source], nodes[target]][link.value].value
source: ns,
target: nt,
value: graph[source][target]
};
});
}));
}, [] as Link[])
const bounding = chartDiv.getBoundingClientRect();
@ -146,6 +152,8 @@
sankey.nodes(nodes).links(links).layout(32);
console.log("here2", nodes, links);
const svg = d3
.select(chartDiv)
.append('svg')
@ -171,6 +179,9 @@
.append('path')
.attr('class', 'link')
.attr('d', path)
.style('stroke', function(d) {
return d3.rgb(color[d.source.index]).toString();
})
.style('stroke-width', function (d) {
return Math.max(1, d.dy);
})
@ -269,7 +280,6 @@
:global(.link) {
fill: none;
stroke: #000;
stroke-opacity: 0.2;
stroke-opacity: 0.5;
}
</style>

View File

@ -0,0 +1,51 @@
<script lang="ts">
import type { Application } from '$lib/ApplicationsStore.svelte';
import { post, preventDefault } from '$lib/utils';
let {
onreload
}: {
onreload: (item: Application) => void;
} = $props();
let dialogElement: HTMLDialogElement;
let link = $state('');
async function createApplication() {
try {
const r: Application = await post('application/link', {
text: link
});
onreload(r);
dialogElement.close()
} catch (e) {
console.log('Inform the user', e);
}
}
function docKey(e: KeyboardEvent) {
if (e.ctrlKey && e.code === 'KeyN') {
dialogElement.showModal();
e.stopPropagation();
e.preventDefault();
return;
}
}
$effect(() => {
document.addEventListener('keydown', docKey, false);
return () => {
document.removeEventListener('keydown', docKey);
};
});
</script>
<dialog class="card max-w-[50vw]" bind:this={dialogElement}>
<form onsubmit={preventDefault(createApplication)}>
<fieldset>
<label class="flabel" for="title">Link</label>
<input class="finput" id="title" bind:value={link} required />
</fieldset>
</form>
</dialog>

View File

@ -0,0 +1,109 @@
<script lang="ts">
import {
ApplicationStatus,
ApplicationStatusMaping,
type Application
} from '$lib/ApplicationsStore.svelte';
import type { AsEnum } from '$lib/ApplicationsStore.svelte';
import { post } from '$lib/utils';
let {
application,
onreload
}: {
application?: Application;
onreload: (item: Application) => void;
} = $props();
let filter = $state('');
let applications: Application[] = $state([]);
let dialogElement: HTMLDialogElement;
async function getApplicationList() {
applications = await post('application/list', {});
}
$effect(() => {
getApplicationList();
});
function docKey(e: KeyboardEvent) {
if (e.ctrlKey && e.code === 'KeyK') {
dialogElement.showModal();
e.stopPropagation();
e.preventDefault();
return;
}
}
$effect(() => {
document.addEventListener('keydown', docKey, false);
return () => {
document.removeEventListener('keydown', docKey);
};
});
</script>
<dialog class="card max-w-[50vw]" bind:this={dialogElement}>
<div class="flex">
<input placeholder="Filter" class="p-2 flex-grow" bind:value={filter} />
<div>
{applications.length}
</div>
</div>
<div class="overflow-y-auto overflow-x-hidden flex-grow p-2">
{#each applications.filter((i) => {
if (application && i.id == application.id) {
return false;
}
if (!filter) {
return true;
}
if (filter.includes('@') && i.company) {
const f = new RegExp(filter.split('@')[0].trim(), 'ig');
const c = new RegExp(filter.split('@')[1].trim(), 'ig');
return i.title.match(f) && i.company.match(c);
}
const f = new RegExp(filter, 'ig');
let x = i.title;
if (i.company) {
x = `${x} @ ${i.company}`;
}
return x.match(f);
}) as item}
<div class="card p-2 my-2 bg-slate-100 max-w-full" role="none">
<button class="text-left max-w-full" type="button" onclick={() => {
dialogElement.close()
onreload(item)
}}>
<h2 class="text-lg text-blue-500 flex justify-between">
<div>
{item.title}
{#if item.company}
<div class="text-violet-800">
@ {item.company}
</div>
{/if}
</div>
<div>
{#if !([ApplicationStatus.ToApply, ApplicationStatus.WorkingOnIt] as AsEnum<ApplicationStatus>[]).includes(item!.status)}
{ApplicationStatusMaping[item.status]}
{/if}
</div>
</h2>
<span
class="text-violet-600 overflow-hidden whitespace-nowrap block max-w-full"
>
{item.url}
</span>
</button>
</div>
{/each}
</div>
</dialog>

View File

@ -3,7 +3,8 @@
ApplicationStatus,
ApplicationStatusMaping,
applicationStore,
type Application
type Application,
type AsEnum
} from '$lib/ApplicationsStore.svelte';
import { put, preventDefault, post, get, deleteR } from '$lib/utils';
@ -14,6 +15,8 @@
import DropZone from './DropZone.svelte';
import { userStore } from '$lib/UserStore.svelte';
import LinkApplication from './LinkApplication.svelte';
import SearchApplication from './SearchApplication.svelte';
import NewApplication from './NewApplication.svelte';
let activeItem: Application | undefined = $state();
@ -24,6 +27,8 @@
let lastExtData: any = $state(undefined);
let autoExtData = false;
let showExtraData = $state(false);
async function activate(item?: Application) {
if (!item) {
return;
@ -113,8 +118,6 @@
}
}
console.log('setting up interest');
window.addEventListener('message', onMessage);
window.postMessage({ type: 'REGISTER_INTEREST' });
return () => {
@ -156,7 +159,7 @@
applicationStore.loadItem = undefined;
});
async function moveStatus(status: number) {
async function moveStatus(status: AsEnum<typeof ApplicationStatus>, moveOut = true) {
if (!activeItem) return;
// Deactivate active item
try {
@ -171,7 +174,11 @@
}
applicationStore.loadApplications(true);
applicationStore.loadAplyed(true);
if (moveOut) {
activeItem = undefined;
} else {
activeItem.status = status;
}
//openedWindow?.close();
//openedWindow = undefined;
}
@ -240,6 +247,20 @@
{statusMapping}
</div>
{/if}
{#if showExtraData}
<fieldset class="max-w-full min-w-0 overflow-hidden">
<div class="flabel">Id</div>
<div class="finput bg-white w-full break-keep">
{activeItem.id}
</div>
</fieldset>
<fieldset class="max-w-full min-w-0 overflow-hidden">
<div class="flabel">Create Time</div>
<div class="finput bg-white w-full break-keep">
{activeItem.create_time}
</div>
</fieldset>
{/if}
<div class="flex gap-2">
<fieldset class="grow">
<label class="flabel" for="title">Company</label>
@ -395,6 +416,13 @@
>
👋
</button>
<button
class:btn-primary={!showExtraData}
class:btn-confirm={showExtraData}
onclick={() => (showExtraData = !showExtraData)}
>
🔬
</button>
</div>
</div>
{#if applicationStore.dragging}
@ -423,13 +451,15 @@
>
Ignore it
</DropZone>
{/if}
{#if [ApplicationStatus.WorkingOnIt, ApplicationStatus.Expired].includes(activeItem.status)}
<!-- Expired -->
<DropZone
icon="clock-fill text-orange-500"
ondrop={() => {
if (activeItem && activeItem.status === ApplicationStatus.Expired) {
moveStatus(ApplicationStatus.ToApply);
moveStatus(ApplicationStatus.ToApply, false);
} else {
moveStatus(ApplicationStatus.Expired);
}
@ -437,7 +467,9 @@
>
Mark as expired
</DropZone>
{/if}
{#if activeItem.status === ApplicationStatus.WorkingOnIt}
<!-- Repeated -->
<DropZone icon="trash-fill text-danger" ondrop={() => remove()}
>Delete it</DropZone
@ -522,3 +554,11 @@
onreload={(item) => (activeItem = item)}
/>
{/if}
<SearchApplication application={activeItem} onreload={(item) => (activeItem = item)} />
<NewApplication
onreload={(item) => {
activate(item);
}}
/>

View File

@ -3,6 +3,9 @@ import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
server: {
host: true,
},
build: {
commonjsOptions: {
esmExternals: true