chore: things done a very nice and usable git commit

This commit is contained in:
Andre Henriques 2024-10-15 12:58:02 +01:00
parent 55434c590a
commit c83b2fd541
12 changed files with 518 additions and 390 deletions

View File

@ -71,7 +71,7 @@ data class Application(
data class SubmitRequest(val text: String)
data class ListRequest(val status: Int?)
data class ListRequest(val status: Int? = null, val views: Boolean? = null)
data class StatusRequest(val id: String, val status: Int)
@ -357,7 +357,11 @@ class ApplicationsController(
}
if (application.unique_url != null) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Application already has unique_url", null)
throw ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Application already has unique_url",
null
)
}
application.original_url = application.url
@ -554,14 +558,13 @@ class ApplicationService(
return true
}
public fun findAll(user: UserDb, info: ListRequest): List<Application> {
private fun internalFindAll(user: UserDb, info: ListRequest): Iterable<Application> {
if (info.status == null) {
return db.query(
"select * from applications where user_id=? order by title asc;",
arrayOf(user.id),
Application
)
.toList()
"select * from applications where user_id=? order by title asc;",
arrayOf(user.id),
Application
)
}
// If it's to apply also remove the linked_application to only show the main
@ -571,7 +574,6 @@ class ApplicationService(
arrayOf(user.id),
Application
)
.toList()
}
return db.query(
@ -579,7 +581,17 @@ class ApplicationService(
arrayOf(user.id, info.status),
Application,
)
.toList()
}
public fun findAll(user: UserDb, info: ListRequest): List<Application> {
var iter = internalFindAll(user, info);
if (info.views == true) {
iter = iter.map {
it.views = viewService.listFromApplicationId(it.id)
it
}
}
return iter.toList()
}
public fun update(application: Application): Application {

View File

@ -1,24 +1,56 @@
package com.andr3h3nriqu3s.applications
import java.sql.ResultSet
import java.sql.Timestamp
import java.util.Date
import java.util.UUID
import org.springframework.http.MediaType
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.RowMapper
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
data class View(var id: String, var application_id: String, var time: Date) {
data class View(var id: String, var application_id: String, var time: Timestamp) {
companion object : RowMapper<View> {
override public fun mapRow(rs: ResultSet, rowNum: Int): View {
return View(
rs.getString("id"),
rs.getString("application_id"),
rs.getDate("time"),
rs.getTimestamp("time"),
)
}
}
}
@RestController
@ControllerAdvice
@RequestMapping("/api/view")
class ViewController(
val sessionService: SessionService,
val applicationService: ApplicationService,
val flairService: FlairService,
val viewService: ViewService,
) {
@GetMapping(path = ["/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun getCV(@PathVariable id: String, @RequestHeader("token") token: String): List<View> {
val user = sessionService.verifyTokenThrow(token)
val application = applicationService.findApplicationById(user, id)
if (application == null) {
throw NotFound()
}
return application.views
}
}
@Service
public class ViewService(val db: JdbcTemplate) {
@ -58,7 +90,7 @@ public class ViewService(val db: JdbcTemplate) {
public fun create(application_id: String): View {
val id = UUID.randomUUID().toString()
var new_view = View(id, application_id, Date())
var new_view = View(id, application_id, Timestamp(Date().getTime()))
db.update(
"insert into views (id, application_id) values (?, ?)",

View File

@ -45,7 +45,7 @@ @layer components {
}
.finput {
@apply rounded-lg w-full p-2 drop-shadow-lg border-gray-300 border;
@apply rounded-lg w-full p-2 drop-shadow-lg border-gray-300 border mb-1;
}
.finput[type='color'] {
@ -74,17 +74,16 @@ @layer components {
}
@print {
@page :footer {
display: none
}
@page :footer {
display: none;
}
@page :header {
display: none
}
@page :header {
display: none;
}
}
@page {
size: auto;
margin: 0;
size: auto;
margin: 0;
}

View File

@ -10,8 +10,9 @@ export const ApplicationStatus = Object.freeze({
ApplyedButSaidNo: 3,
Applyed: 4,
Expired: 5,
TasksToDo: 6,
LinkedApplication: 7
TasksToDo: 6,
LinkedApplication: 7,
InterviewStep1: 8
});
export const ApplicationStatusMaping: Record<
@ -24,15 +25,16 @@ export const ApplicationStatusMaping: Record<
3: 'Applyed But Said No',
4: 'Applyed',
5: 'Expired',
6: 'Tasks To Do',
7: 'Linked Application',
6: 'Tasks To Do',
7: 'Linked Application',
8: 'Interview 1'
});
export type View = {
id: string,
application_id: string,
time: string,
}
id: string;
application_id: string;
time: string;
};
export type Application = {
id: string;
@ -44,25 +46,25 @@ export type Application = {
extra_data: string;
payrange: string;
status: AsEnum<typeof ApplicationStatus>;
recruiter: string;
company: string;
message: string;
linked_application: string;
application_time: string;
create_time: string;
status_history: string;
recruiter: string;
company: string;
message: string;
linked_application: string;
application_time: string;
create_time: string;
status_history: string;
flairs: Flair[];
views: View[];
views: View[];
};
function createApplicationStore() {
let applications: Application[] = $state([]);
let applyed: Application[] = $state([]);
let tasksToDo: Application[] = $state([]);
let all: Application[] = $state([]);
let dragApplication: Application | undefined = $state(undefined);
let loadItem: Application | undefined = $state(undefined);
let loadItem: Application | undefined = $state(undefined);
return {
/**
@ -82,24 +84,27 @@ function createApplicationStore() {
if (!force && applyed.length > 1) {
return;
}
applyed = await post('application/list', { status: ApplicationStatus.Applyed });
applyed = await post('application/list', { status: ApplicationStatus.Applyed, views: true });
},
/**
* @throws {Error}
*/
async loadTasksToDo(force = false) {
if (!force && tasksToDo.length > 1) {
async loadAll(force = false) {
if (!force && all.length > 1) {
return;
}
tasksToDo = await post('application/list', { status: ApplicationStatus.TasksToDo });
all = await post('application/list', {});
},
clear() {
applications = [];
},
dragStart(application: Application) {
dragStart(application: Application | undefined) {
if (!application) {
return;
}
dragApplication = application;
},
@ -119,17 +124,17 @@ function createApplicationStore() {
return applyed;
},
get tasksToDo() {
return tasksToDo;
},
get all() {
return all;
},
get loadItem() {
return loadItem;
},
get loadItem() {
return loadItem;
},
set loadItem(item: Application | undefined) {
loadItem = item;
},
set loadItem(item: Application | undefined) {
loadItem = item;
}
};
}

View File

@ -4,7 +4,8 @@
import ApplicationsList from './ApplicationsList.svelte';
import WorkArea from './work-area/WorkArea.svelte';
import AppliyedList from './AppliyedList.svelte';
import TasksToDoList from './TasksToDoList.svelte';
import PApplicationList from './PApplicationList.svelte';
import { ApplicationStatus } from '$lib/ApplicationsStore.svelte';
</script>
<HasUser redirect="/cv">
@ -15,7 +16,12 @@
<ApplicationsList />
<WorkArea />
</div>
<TasksToDoList />
<PApplicationList status={ApplicationStatus.InterviewStep1}>
Interview I
</PApplicationList >
<PApplicationList status={ApplicationStatus.TasksToDo}>
Tasks To do
</PApplicationList >
<AppliyedList />
</div>
</div>

View File

@ -33,7 +33,7 @@
return x.match(f);
}) as item}
<div
class="card p-2 my-2 bg-slate-100"
class="card p-2 my-2 bg-slate-100 max-w-full"
draggable="true"
ondragstart={() => applicationStore.dragStart(item)}
ondragend={() => {
@ -43,21 +43,26 @@
}}
role="none"
>
<div class:animate-pulse={applicationStore.dragging?.id === item.id}>
<h2 class="text-lg text-blue-500">
{item.title}
{#if item.company}
<div class="text-violet-800" >
@ {item.company}
<div class="max-w-full" class:animate-pulse={applicationStore.dragging?.id === item.id}>
<h2 class="text-lg text-blue-500 flex gap-2 max-w-full overflow-hidden">
<div class="flex-grow max-w-[90%]">
<div class="whitespace-nowrap overflow-hidden">
{item.title}
</div>
{/if}
{#if item.company}
<div class="text-violet-800">
@ {item.company}
</div>
{/if}
</div>
<div>
{#if item.url.includes('linkedin')}
<span class="bi bi-linkedin"></span>
{:else if item.url.includes('glassdoor')}
<span class="bi bi-eyeglasses"></span>
{/if}
</div>
</h2>
<a
href={item.url}
class="text-violet-600 overflow-hidden whitespace-nowrap block"
>
{item.url}
</a>
</div>
</div>
{/each}

View File

@ -5,14 +5,14 @@
<div class="p-7">
<div class="card p-2 rounded-xl flex">
<button class="text-secudanry hover:text-primary px-2" onclick={() => goto('/')}> Home </button>
<button class="text-secudanry hover:text-primary px-2" onclick={() => goto('/submit')}>
<button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/')}> Home </button>
<button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/submit')}>
Submit text
</button>
<button class="text-secudanry hover:text-primary px-2" onclick={() => goto('/flair')}>
<button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/flair')}>
Flair
</button>
<button class="text-secudanry hover:text-primary px-2" onclick={() => goto('/graphs')}>
<button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/graphs')}>
Graphs
</button>
<div class="flex-grow"></div>

View File

@ -1,20 +1,32 @@
<script lang="ts">
import { applicationStore } from '$lib/ApplicationsStore.svelte';
import { onMount } from 'svelte';
import {
ApplicationStatus,
applicationStore,
type AsEnum
} from '$lib/ApplicationsStore.svelte';
import { get } from '$lib/utils';
import { onMount, type Snippet } from 'svelte';
let { children, status }: { children: Snippet; status: AsEnum<typeof ApplicationStatus> } =
$props();
let applications = $derived(applicationStore.all.filter((item) => item.status == status))
onMount(() => {
applicationStore.loadTasksToDo();
applicationStore.loadAll();
});
</script>
{#if applicationStore.tasksToDo.length > 0}
{#if applications.length > 0}
<div class="card p-3 rounded-lg flex flex-col">
<h1>Tasks To Do</h1>
<h1>{@render children()}</h1>
<div class="overflow-auto flex-grow">
{#each applicationStore.tasksToDo as item}
{#each applications as item}
<button
class="card p-2 my-2 bg-slate-100 w-full text-left"
onclick={() => {
onclick={async () => {
item.views = await get(`view/${item.id}`);
applicationStore.loadItem = item;
window.scrollTo({
top: 0,

View File

@ -154,7 +154,7 @@
class="flex-grow text-blue-500 print:hidden"
bind:value={otherSearch}
/>
<span class="hidden print:inline text-slate-600 text-sm"><a href="https://www.andr3h3nriqu3s.com/cv?id={id}">Looking for other skills?</a></span>
<span class="hidden print:inline text-slate-600 text-sm"><a href="https://www.andr3h3nriqu3s.com/cv?id={id}">Search other skills!</a></span>
{/if}
</h1>
<div class="flex flex-wrap gap-2 py-2">

View File

@ -9,242 +9,289 @@
let applications: Application[] = $state([]);
let chartDiv: HTMLDivElement;
let chartDiv: HTMLDivElement | undefined = $state();
let showExpired = $state(false);
let showIgnore = $state(false);
let showLinked = $state(false);
let showToApply = $state(false);
// Handle the graph creation
$effect(() => {
if (!chartDiv || applications.length == 0) return;
chartDiv.innerHTML = '';
type NodeType =
| AsEnum<typeof ApplicationStatus>
| 'Linkedin'
| 'Glassdoor'
| 'Direct Source';
let graph = {} as Record<string, Record<string, number>>;
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;
}
let sourceData: Record<string, number> = {
'Direct Source': 0,
Glassdoor: 0,
Linkedin: 0
};
applications.forEach((a) => {
let source: NodeType;
if (!showExpired && a.status == ApplicationStatus.Expired) {
return;
}
if (!showIgnore && a.status == ApplicationStatus.Ignore) {
return;
}
if (!showLinked && a.status == ApplicationStatus.LinkedApplication) {
return;
}
if (
!showToApply &&
(a.status == ApplicationStatus.ToApply || a.status == ApplicationStatus.WorkingOnIt)
) {
return;
}
if (a.url.includes('linkedin')) {
source = 'Linkedin';
sourceData['Linkedin'] += 1;
} else if (a.url.includes('glassdoor')) {
source = 'Glassdoor';
sourceData['Glassdoor'] += 1;
} else {
source = 'Direct Source';
sourceData['Direct Source'] += 1;
}
if (
(
[
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;
}
source = addGraph(source, ApplicationStatus.Applyed);
const history = a.status_history.split(',');
if (history.includes(`${ApplicationStatus.TasksToDo}`)) {
source = addGraph(source, ApplicationStatus.TasksToDo);
if (a.status == ApplicationStatus.TasksToDo) {
return;
}
}
if (history.includes(`${ApplicationStatus.InterviewStep1}`)) {
source = addGraph(source, ApplicationStatus.InterviewStep1);
if (a.status == ApplicationStatus.InterviewStep1) {
return;
}
}
addGraph(source, ApplicationStatus.ApplyedButSaidNo);
});
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 = sourceData[node] ?? getGraphValueFor(node);
const base = {
value: value,
originalValue: node,
id: name,
index: i,
percentage: Math.trunc((value / applications.length) * 100)
};
return base;
});
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: ns,
target: nt,
value: graph[source][target]
};
})
);
}, [] as Link[]);
const bounding = chartDiv.getBoundingClientRect();
let sankey = mySankey()
.nodeWidth(20)
.nodePadding(10)
.size([bounding.width, bounding.height - 20]);
let path = sankey.link();
sankey.nodes(nodes).links(links).layout(32);
const svg = d3
.select(chartDiv)
.append('svg')
.attr('width', bounding.width)
.attr('height', bounding.height)
.attr('viewBox', [0, 0, bounding.width, bounding.height])
.attr('style', 'max-width: 100%; height: auto; height: intrinsic;');
// let color = d3.schemeSpectral[nodes.length];
// let color = d3.interpolateTurbo(nodes.length);
function getColor(index: number) {
return d3.interpolateRainbow(index/nodes.length);
}
// add in the links
var link = svg
.append('g')
.selectAll('.link')
.data(
links as ((typeof links)[0] & {
dy: number;
source: (typeof nodes)[0];
target: (typeof nodes)[0];
})[]
)
.enter()
.append('path')
.attr('class', 'link')
.attr('d', path)
.style('stroke', function (d) {
return d3.rgb(
getColor(d.source.index)
// color[d.source.index]
).toString();
})
.style('stroke-width', function (d) {
return Math.max(1, d.dy);
})
.sort(function (a, b) {
return b.dy - a.dy;
});
// add the link titles
link.append('title').text(function (d) {
return d.source.id + ' → ' + d.target.id + '\n' + d.value;
});
const node = svg
.append('g')
.selectAll('.node')
.data(
nodes as ((typeof nodes)[0] & {
x: number;
y: number;
dy: number;
dx: number;
index: number;
})[]
)
.enter()
.append('g')
.attr('class', 'node')
.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
node.append('rect')
.attr('height', function (d) {
return d.dy;
})
.attr('width', sankey.getNodeWidth())
.style('fill', function (d) {
return getColor(d.index);
//color[d.index];
})
.style('stroke', (d) => {
return d3.rgb(
getColor(d.index)
//color[d.index]
).darker(2).toString();
})
.append('title')
.text(function (d) {
return d.id + '\n' + d.value;
});
node.append('text')
.attr('x', -6)
.attr('y', function (d) {
return d.dy / 2;
})
.attr('dy', '.35em')
.attr('text-anchor', 'end')
.attr('transform', null)
.text(function (d) {
return `${d.id} (${d.value} ${d.percentage}%)`;
})
.filter(function (d) {
return d.x < bounding.width / 2;
})
.attr('x', 6 + sankey.getNodeWidth())
.attr('text-anchor', 'start');
});
async function getData() {
try {
applications = await post('application/list', {});
chartDiv.innerHTML = '';
type NodeType =
| AsEnum<typeof ApplicationStatus>
| 'Linkedin'
| 'Glassdoor'
| 'Direct Source';
let graph = {} as Record<string, Record<string, number>>;
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')) {
source = 'Linkedin';
} else if (a.url.includes('glassdoor')) {
source = 'Glassdoor';
} else {
source = 'Direct Source';
}
if (
(
[
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
)
) {
addGraph(source, ApplicationStatus.Applyed);
addGraph(ApplicationStatus.Applyed, a.status);
}
});
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;
});
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: ns,
target: nt,
value: graph[source][target]
};
}));
}, [] as Link[])
const bounding = chartDiv.getBoundingClientRect();
let sankey = mySankey()
.nodeWidth(20)
.nodePadding(10)
.size([bounding.width, bounding.height - 20]);
let path = sankey.link();
sankey.nodes(nodes).links(links).layout(32);
console.log("here2", nodes, links);
const svg = d3
.select(chartDiv)
.append('svg')
.attr('width', bounding.width)
.attr('height', bounding.height)
.attr('viewBox', [0, 0, bounding.width, bounding.height])
.attr('style', 'max-width: 100%; height: auto; height: intrinsic;');
let color = d3.schemeSpectral[nodes.length];
// add in the links
var link = svg
.append('g')
.selectAll('.link')
.data(
links as ((typeof links)[0] & {
dy: number;
source: (typeof nodes)[0];
target: (typeof nodes)[0];
})[]
)
.enter()
.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);
})
.sort(function (a, b) {
return b.dy - a.dy;
});
// add the link titles
link.append('title').text(function (d) {
return d.source.id + ' → ' + d.target.id + '\n' + d.value;
});
const node = svg
.append('g')
.selectAll('.node')
.data(
nodes as ((typeof nodes)[0] & {
x: number;
y: number;
dy: number;
dx: number;
index: number;
})[]
)
.enter()
.append('g')
.attr('class', 'node')
.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
node.append('rect')
.attr('height', function (d) {
return d.dy;
})
.attr('width', sankey.getNodeWidth())
.style('fill', function (d) {
return color[d.index];
})
.style('stroke', (d) => {
return d3.rgb(color[d.index]).darker(2).toString();
})
.append('title')
.text(function (d) {
return d.id + '\n' + d.value;
});
node.append('text')
.attr('x', -6)
.attr('y', function (d) {
return d.dy / 2;
})
.attr('dy', '.35em')
.attr('text-anchor', 'end')
.attr('transform', null)
.text(function (d) {
return `${d.id} (${d.value} ${d.end ? `${d.percentage}%` : ''})`;
})
.filter(function (d) {
return d.x < bounding.width / 2;
})
.attr('x', 6 + sankey.getNodeWidth())
.attr('text-anchor', 'start');
} catch (e) {
console.log('TODO, inform the user', e);
}
@ -258,7 +305,27 @@
<HasUser redirect="/cv">
<div class="flex flex-col h-[100vh]">
<NavBar />
<div class="flex flex-grow p-5">
<div class="p-1 px-5">
<div class="bg-white p-3 rounded-lg gap-5 flex">
<fieldset>
<label for="showIgnore">Show Ignore</label>
<input id="showIgnore" type="checkbox" bind:checked={showIgnore} />
</fieldset>
<fieldset>
<label for="showExpired">Show Expired</label>
<input id="showExpired" type="checkbox" bind:checked={showExpired} />
</fieldset>
<fieldset>
<label for="showLinked">Show Linked Applicaitons</label>
<input id="showLinked" type="checkbox" bind:checked={showLinked} />
</fieldset>
<fieldset>
<label for="showToApply">Show To Apply</label>
<input id="showToApply" type="checkbox" bind:checked={showToApply} />
</fieldset>
</div>
</div>
<div class="flex flex-grow flex-col p-5">
<div class="bg-white p-3 rounded-lg" style="width: 100%; height: 100%">
<div bind:this={chartDiv} style="width: 100%; height: 100%;"></div>
</div>

View File

@ -1,10 +1,5 @@
<script lang="ts">
import {
ApplicationStatus,
ApplicationStatusMaping,
type Application
} from '$lib/ApplicationsStore.svelte';
import type { AsEnum } from '$lib/ApplicationsStore.svelte';
import { ApplicationStatusMaping, type Application } from '$lib/ApplicationsStore.svelte';
import { post } from '$lib/utils';
let {
@ -24,12 +19,10 @@
applications = await post('application/list', {});
}
$effect(() => {
getApplicationList();
});
function docKey(e: KeyboardEvent) {
if (e.ctrlKey && e.code === 'KeyK') {
applications = [];
getApplicationList();
dialogElement.showModal();
e.stopPropagation();
e.preventDefault();
@ -53,6 +46,7 @@
</div>
</div>
<div class="overflow-y-auto overflow-x-hidden flex-grow p-2">
<!-- TODO loading screen -->
{#each applications.filter((i) => {
if (application && i.id == application.id) {
return false;
@ -62,8 +56,9 @@
}
if (filter.includes('@') && i.company) {
const f = new RegExp(filter.split('@')[0].trim(), 'ig');
const c = new RegExp(filter.split('@')[1].trim(), 'ig');
const splits = filter.split('@');
const f = new RegExp(splits[0].trim(), 'ig');
const c = new RegExp(splits[1].trim(), 'ig');
return i.title.match(f) && i.company.match(c);
}
@ -78,10 +73,14 @@
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)
}}>
<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}
@ -95,9 +94,7 @@
{ApplicationStatusMaping[item.status]}
</div>
</h2>
<span
class="text-violet-600 overflow-hidden whitespace-nowrap block max-w-full"
>
<span class="text-violet-600 overflow-hidden whitespace-nowrap block max-w-full">
{item.url}
</span>
</button>

View File

@ -93,6 +93,21 @@
);
}
function docKey(e: KeyboardEvent) {
if (activeItem && e.ctrlKey && e.code === 'KeyO') {
openCV(activeItem.id);
e.stopPropagation();
e.preventDefault();
return;
}
}
$effect(() => {
document.addEventListener('keydown', docKey, false);
return () => {
document.removeEventListener('keydown', docKey);
};
});
async function loadActive() {
try {
activeItem = await get('application/active');
@ -264,48 +279,30 @@
<div class="flex gap-2">
<fieldset class="grow">
<label class="flabel" for="title">Company</label>
<input
class="finput"
id="title"
bind:value={activeItem.company}
onchange={save}
/>
<input class="finput" id="title" bind:value={activeItem.company} onchange={save} />
</fieldset>
<fieldset class="grow">
<label class="flabel" for="title">Recruiter</label>
<input
class="finput"
id="title"
bind:value={activeItem.recruiter}
onchange={save}
/>
<input class="finput" id="title" bind:value={activeItem.recruiter} onchange={save} />
</fieldset>
</div>
<fieldset>
<label class="flabel" for="title">Title</label>
<input
class="finput"
id="title"
bind:value={activeItem.title}
onchange={save}
/>
<input class="finput" id="title" bind:value={activeItem.title} onchange={save} />
</fieldset>
<fieldset>
<label class="flabel" for="payrange">Pay Range</label>
<input
class="finput"
id="payrange"
bind:value={activeItem.payrange}
onchange={save}
/>
<input class="finput" id="payrange" bind:value={activeItem.payrange} onchange={save} />
</fieldset>
<fieldset draggable="false" class="max-w-full min-w-0 overflow-hidden">
<div class="flabel">Url</div>
<div class="finput bg-white w-full break-keep">
{activeItem.url}
</div>
</fieldset>
{#if activeItem.unique_url && activeItem.unique_url !== activeItem.url}
{#if !activeItem.unique_url || showExtraData}
<fieldset draggable="false" class="max-w-full min-w-0 overflow-hidden">
<div class="flabel">Url</div>
<div class="finput bg-white w-full break-keep">
{activeItem.url}
</div>
</fieldset>
{/if}
{#if activeItem.unique_url}
<fieldset draggable="false">
<div class="flabel">Unique Url</div>
<div class="finput bg-white">
@ -344,11 +341,7 @@
</div>
<fieldset>
<label class="flabel" for="extra">Extra Info</label>
<textarea
class="finput"
id="extra"
bind:value={activeItem.extra_data}
onchange={save}
<textarea class="finput" id="extra" bind:value={activeItem.extra_data} onchange={save}
></textarea>
</fieldset>
<fieldset>
@ -398,9 +391,7 @@
</button>
{/if}
{#if activeItem.original_url == null}
<button class="btn-primary" onclick={() => changeUrl.showModal()}>
Update Url
</button>
<button class="btn-primary" onclick={() => changeUrl.showModal()}> Update Url </button>
{/if}
<div class="px-10"></div>
<button class="btn-primary" onclick={() => linkApplication.showModal()}>
@ -409,11 +400,7 @@
{#if activeItem.original_url != null}
<button class="btn-danger" onclick={resetUrl}> Reset Url </button>
{/if}
<button
class:btn-primary={drag}
class:btn-danger={!drag}
onclick={() => (drag = !drag)}
>
<button class:btn-primary={drag} class:btn-danger={!drag} onclick={() => (drag = !drag)}>
👋
</button>
<button
@ -435,7 +422,7 @@
ondrop={() => {
moveStatus(ApplicationStatus.ToApply);
applicationStore.loadAplyed(true);
applicationStore.loadTasksToDo(true);
applicationStore.loadAll(true);
}}
>
To apply
@ -471,9 +458,7 @@
{#if activeItem.status === ApplicationStatus.WorkingOnIt}
<!-- Repeated -->
<DropZone icon="trash-fill text-danger" ondrop={() => remove()}
>Delete it</DropZone
>
<DropZone icon="trash-fill text-danger" ondrop={() => remove()}>Delete it</DropZone>
{/if}
{#if [ApplicationStatus.WorkingOnIt, ApplicationStatus.TasksToDo].includes(activeItem.status)}
@ -482,8 +467,8 @@
icon="server text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.Applyed);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
applicationStore.loadTasksToDo(true);
}}
>
Apply
@ -496,12 +481,24 @@
icon="server text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.TasksToDo);
applicationStore.loadTasksToDo(true);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Tasks To Do
</DropZone>
<!-- Tasks to do -->
<DropZone
icon="server text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.InterviewStep1);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Interview 1
</DropZone>
{/if}
<!-- Rejected -->
@ -557,8 +554,4 @@
<SearchApplication application={activeItem} onreload={(item) => (activeItem = item)} />
<NewApplication
onreload={(item) => {
activate(item);
}}
/>
<NewApplication onreload={activate} />