chore: various updates

This commit is contained in:
2025-03-26 13:51:49 +00:00
parent 941875ff21
commit eea2d6c3df
24 changed files with 1527 additions and 236 deletions

View File

@@ -3,87 +3,87 @@
@tailwind utilities;
@font-face {
font-family: 'JetBrainsMono';
src: url('/fonts/JetBrainsMono-VariableFont_wght.ttf');
font-family: 'JetBrainsMono';
src: url('/fonts/JetBrainsMono-VariableFont_wght.ttf');
}
@layer components {
.grad-back {
@apply bg-gradient-to-t from-violet-700 via-blue-400 to-violet-700;
background-size: 100vw 400vh;
animation: grad-back 100s linear infinite;
}
.grad-back {
@apply bg-gradient-to-t from-violet-700 via-blue-400 to-violet-700;
background-size: 100vw 400vh;
animation: grad-back 100s linear infinite;
}
@keyframes grad-back {
from {
background-position: 0 0;
}
@keyframes grad-back {
from {
background-position: 0 0;
}
to {
background-position: 0 400vh;
}
}
to {
background-position: 0 400vh;
}
}
.font-JetBrainsMono {
font-family: 'JetBrainsMono';
}
.font-JetBrainsMono {
font-family: 'JetBrainsMono';
}
h1 {
@apply text-purple-500 font-bold font-JetBrainsMono text-xl;
}
h1 {
@apply text-purple-500 font-bold font-JetBrainsMono text-xl;
}
dialog {
@apply p-3 rounded-md;
}
dialog {
@apply p-3 rounded-md;
}
dialog::backdrop {
@apply bg-secudanry opacity-75 fixed top-0 right-0 bottom-0 left-0;
}
dialog::backdrop {
@apply bg-secudanry opacity-75 fixed top-0 right-0 bottom-0 left-0;
}
.flabel {
@apply block text-purple-500;
}
.flabel {
@apply block text-purple-500;
}
.finput {
@apply rounded-lg w-full p-2 drop-shadow-lg border-gray-300 border mb-1;
}
.finput {
@apply rounded-lg w-full p-2 drop-shadow-lg border-gray-300 border mb-1 bg-white;
}
.finput[type='color'] {
@apply p-0;
}
.finput[type='color'] {
@apply p-0;
}
.btns {
@apply flex justify-center py-2 gap-2;
}
.btns {
@apply flex justify-center py-2 gap-2;
}
.btn-primary {
@apply rounded-lg text-white bg-violet-500 p-2;
}
.btn-primary {
@apply rounded-lg text-white bg-violet-500 p-2;
}
.btn-danger {
@apply rounded-lg bg-danger p-2 text-white;
}
.btn-danger {
@apply rounded-lg bg-danger p-2 text-white;
}
.btn-confirm {
@apply rounded-lg bg-blue-400 text-white p-2;
}
.btn-confirm {
@apply rounded-lg bg-blue-400 text-white p-2;
}
.card {
@apply bg-white rounded-lg drop-shadow-lg;
}
.card {
@apply bg-white rounded-lg drop-shadow-lg;
}
}
@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

@@ -33,6 +33,7 @@ export type Application = {
linked_application: string;
create_time: string;
status_history: string;
job_level: string;
flairs: Flair[];
events: ApplicationEvent[];
};

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { applicationStore } from '$lib/ApplicationsStore.svelte';
import { applicationStore, type Application } from '$lib/ApplicationsStore.svelte';
import { get } from '$lib/utils';
import { onMount } from 'svelte';
let filter = $state('');
@@ -28,15 +29,57 @@
return x.match(f);
})
);
let gettingNext = $state(false);
async function getNext() {
gettingNext = true;
try {
const r: Application[] = await get('mail/getNext');
for (const app of r) {
applicationStore.all.push(app);
}
if (r.length > 0) {
applicationStore.loadItem = r[0];
}
} catch (e) {
console.error('TODO inform user', e);
} finally {
gettingNext = false;
}
}
function docKey(e: KeyboardEvent) {
if (e.ctrlKey && e.code === 'KeyJ' && internal.length > 0) {
applicationStore.loadItem = internal[0];
e.stopPropagation();
e.preventDefault();
return;
}
}
$effect(() => {
document.addEventListener('keydown', docKey, false);
return () => {
document.removeEventListener('keydown', docKey);
};
});
</script>
<div class="w-2/12 card p-3 flex flex-col flex-shrink min-h-0">
<h1>To Apply</h1>
<div class="flex pb-2">
<div class="flex pb-2 items-center">
<input placeholder="Filter" class="p-2 flex-grow" bind:value={filter} />
<div>
{internal.length}
</div>
{#if !gettingNext}
<button class="p-2 text-violet-500" onclick={() => getNext()}>
<span class="bi bi-send-arrow-down"></span>
</button>
{:else}
<span class="bi bi-arrow-repeat animate-spin"></span>
{/if}
</div>
<div class="overflow-auto flex-grow p-2">
{#each internal as item}
@@ -51,7 +94,10 @@
}}
role="none"
>
<div class="max-w-full" class:animate-pulse={applicationStore.dragging?.id === item.id}>
<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">

View File

@@ -0,0 +1,555 @@
<script lang="ts">
import { applicationStore, type Application } from '$lib/ApplicationsStore.svelte';
import HasUser from '$lib/HasUser.svelte';
import { onMount } from 'svelte';
import NavBar from '../NavBar.svelte';
import Pie from './Pie.svelte';
import { statusStore } from '$lib/Types.svelte';
import * as d3 from 'd3';
import { goto } from '$app/navigation';
import { get } from '$lib/utils';
import { flairStore } from '$lib/FlairStore.svelte';
onMount(() => {
applicationStore.loadAll();
statusStore.load();
});
let sort: 'asc' | 'desc' = $state('desc');
const payranges = $derived.by(() => {
const obj = applicationStore.all
.filter((a) => a.payrange.match(/\d/))
.map((a) => {
const payrange = a.payrange
.replace(/[kK]/g, '000')
// The first is a - the other is unicode 8211
.replace(/\.\d+/g, '')
.replace(/[^\d\-]/g, '')
.replace(//g, '-')
.split('-')
.map((a) => Number(a));
if (Number.isNaN(payrange[0])) {
payrange[0] = 0;
}
if (Number.isNaN(payrange[1])) {
payrange[1] = 0;
}
return { ...a, payrange };
})
.reduce(
(acc, a) => {
if (!acc[a.company]) {
acc[a.company] = [a];
} else {
acc[a.company].push(a);
}
return acc;
},
{} as Record<string, (Omit<Application, 'payrange'> & { payrange: number[] })[]>
);
return Object.keys(obj)
.reduce(
(acc, a) => {
acc.push([a, obj[a]]);
return acc;
},
[] as [string, (Omit<Application, 'payrange'> & { payrange: number[] })[]][]
)
.toSorted((a, b) => {
const rangesA = a[1].reduce(max_and_min_reducer, [
Number.POSITIVE_INFINITY,
Number.NEGATIVE_INFINITY
]);
const rangesB = b[1].reduce(max_and_min_reducer, [
Number.POSITIVE_INFINITY,
Number.NEGATIVE_INFINITY
]);
const va = (rangesA[1] + rangesA[0]) / 2;
const vb = (rangesB[1] + rangesB[0]) / 2;
if (sort === 'asc') {
return va - vb;
} else if (sort === 'desc') {
return vb - va;
}
return 0;
});
});
let payRangeDiv: HTMLDivElement | undefined = $state(undefined);
function max_and_min_reducer(
acc: [number, number],
a: Omit<Application, 'payrange'> & { payrange: number[] }
): [number, number] {
/*if (a.payrange[0] > 1000000 || a.payrange[1] > 1000000) {
console.log(a);
}*/
return [
Math.min(acc[0], a.payrange[0]),
Math.max(acc[1], a.payrange[1] ?? 0, a.payrange[0])
];
}
const scale = $derived.by(() => {
if (!payRangeDiv) return;
const max_and_min = Object.values(payranges).reduce(
(acc, a) => {
return a[1].reduce(
(acc2, e) => max_and_min_reducer(acc2, e),
acc as [number, number]
);
},
[Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY] as [number, number]
);
const box = payRangeDiv.getBoundingClientRect();
const scale = d3
.scaleLinear()
.domain([max_and_min[0], max_and_min[1]])
.range([0, box.width - 40]);
return scale;
});
const context = (() => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) return null;
context.font = '12px Open sans';
return context;
})();
let open = $state<string | undefined>(undefined);
let searchPayranges = $state('');
let expandPayRanges = $state(false);
let searchPayRangeMode: 'limit' | 'goto' = $state('goto');
let indexPayRanges: HTMLDivElement[] = $state([]);
let gotoIndex: number | undefined = $state(undefined);
$effect(() => {
if (searchPayRangeMode !== 'goto' || !searchPayranges) {
gotoIndex = undefined;
return;
}
let i = 0;
for (let [company] of payranges) {
if (company.match(new RegExp(searchPayranges, 'i'))) {
indexPayRanges[i].scrollIntoView({
behavior: 'smooth',
inline: 'center',
block: 'center'
});
gotoIndex = i;
return;
}
i += 1;
}
gotoIndex = undefined;
});
let flairStats: 'loading' | Record<string, number> = $state('loading');
onMount(async () => {
const items: any[] = await get('flair/stats');
flairStats = items.reduce(
(acc, a) => {
acc[a.name] = a.count;
return acc;
},
{} as Record<string, number>
);
});
</script>
<HasUser redirect="/cv">
<div class="flex flex-col h-[100vh]">
<NavBar />
<div class="p-1 px-5">
<div class="bg-white p-3 rounded-lg gap-5 flex flex-col">
<div class="flex gap-5 flex-wrap">
<Pie
title={'Origin'}
data={applicationStore.all.reduce(
(acc, item) => {
if (item.url.includes('linkedin')) {
acc.linkedin += 1;
} else if (item.url.includes('glassdoor')) {
acc.glassdoor += 1;
} else {
acc.other += 1;
}
return acc;
},
{
linkedin: 0,
glassdoor: 0,
other: 0
}
)}
/>
<Pie
title={'Status'}
data={[...statusStore.nodes, 'Created' as const].reduce(
(acc, item) => {
if (item === 'Created') {
acc[item] = applicationStore.all.filter(
(a) => a.status_id === null
).length;
return acc;
}
acc[item.name] = applicationStore.all.filter(
(a) => item.id === a.status_id
).length;
return acc;
},
{} as Record<string, number>
)}
/>
<Pie
title={'Higher range Pay Range'}
data={applicationStore.all
.filter((a) => a.payrange.match(/\d/))
.map((a) => {
const payrange = a.payrange
.replace(/[kK]/g, '000')
.replace(/[^\d\-]/g, '')
.replace(//g, '-')
.split('-');
return Number(payrange[payrange.length - 1]);
})
.reduce(
(acc, a) => {
const f = Math.floor(a / 10000);
let name = `${f * 10}K-${(f + 1) * 10}K`;
if (f == 0) {
name = '<10K';
}
if (acc[name]) {
acc[name] += 1;
} else {
acc[name] = 1;
}
return acc;
},
{} as Record<string, number>
)}
/>
<Pie
title={'Lower range Pay Range'}
data={applicationStore.all
.filter((a) => a.payrange.match(/\d/))
.map((a) => {
const payrange = a.payrange
.replace(/[kK]/g, '000')
// The first is a - the other is unicode 8211
.replace(/[^\d\-]/g, '')
.replace(//g, '-')
.split('-');
return Number(payrange[0]);
})
.reduce(
(acc, a) => {
const f = Math.floor(a / 10000);
let name = `${f * 10}K-${(f + 1) * 10}K`;
if (f == 0) {
name = '<10K';
}
if (acc[name]) {
acc[name] += 1;
} else {
acc[name] = 1;
}
return acc;
},
{} as Record<string, number>
)}
/>
</div>
{#if flairStats !== 'loading'}
<div class="flex gap-5">
<Pie title={'Flair stats'} data={flairStats} sensitivity={0.02} />
<div class="max-h-[500px] overflow-auto">
<ul>
{#each Object.keys(flairStats).toSorted((a, b) => flairStats[b] - flairStats[a]) as flair}
<li>{flair}: {flairStats[flair]}</li>
{/each}
</ul>
</div>
<Pie
title={'Job Level'}
data={applicationStore.all.reduce(
(acc, a) => {
const job_level = a.job_level ? a.job_level : 'Unknown';
if (acc[job_level]) {
acc[job_level] += 1;
} else {
acc[job_level] = 1;
}
return acc;
},
{} as Record<string, number>
)}
sensitivity={0.02}
/>
</div>
{/if}
<h1 class="text-black">
Pay range
<button
title="Expand/Contract"
onclick={() => {
expandPayRanges = !expandPayRanges;
}}
>
<span
class={!expandPayRanges
? 'bi bi-arrows-angle-expand'
: 'bi bi-arrows-angle-contract'}
></span>
</button>
<button
title="Sorting"
onclick={() => {
if (sort === 'asc') {
sort = 'desc';
} else if (sort === 'desc') {
sort = 'asc';
}
}}
>
<span class={sort === 'asc' ? 'bi bi-arrow-down' : 'bi bi-arrow-up'}></span>
</button>
<button
title="Filter mode"
onclick={() => {
if (searchPayRangeMode === 'limit') {
searchPayRangeMode = 'goto';
} else if (searchPayRangeMode === 'goto') {
searchPayRangeMode = 'limit';
}
}}
>
<span
class={searchPayRangeMode === 'limit'
? 'bi bi-funnel'
: 'bi bi-sort-alpha-down'}
></span>
</button>
</h1>
<div
class="bg-white {expandPayRanges
? ''
: 'min-h-[500px] max-h-[500px]'} overflow-y-auto"
>
<div class="sticky top-0 py-2 px-2 bg-white w-full z-50">
<input
class="w-full z-20"
bind:value={searchPayranges}
placeholder="search"
/>
</div>
<div bind:this={payRangeDiv}>
{#if scale && context}
{#each searchPayranges && searchPayRangeMode === 'limit' ? payranges.filter( (a) => a[0].match(new RegExp(searchPayranges, 'i')) ) : payranges as v, index}
{@const company = v[0]}
{@const values = v[1]}
{@const ranges = values.reduce(max_and_min_reducer, [
Number.POSITIVE_INFINITY,
Number.NEGATIVE_INFINITY
])}
{@const nameCompany = company === '' ? 'No Company' : company}
{#if open !== company}
<div
class="relative h-[40px] pointer-cursor {gotoIndex === index
? 'bg-purple-200/50'
: index % 2 === 0
? 'bg-slate-50'
: ''}"
role="button"
onclick={() => (open = company)}
onkeydown={() => (open = company)}
bind:this={indexPayRanges[index]}
tabindex={1}
>
<div
class="bg-blue-500 w-[20px] h-[10px] rounded-full absolute"
style="left: {10 +
scale(ranges[0]) +
10}px; top: 50%; transform: translateY(-50%); width: {scale(
ranges[1]
) - scale(ranges[0])}px;"
></div>
<div
class="bg-blue-500 w-[20px] h-[20px] rounded-full absolute"
title={`pay: ${ranges[0].toLocaleString('en-GB', {
notation: 'compact',
currency: 'GBP',
style: 'currency'
})}`}
style="left: {10 +
scale(
ranges[0]
)}px; top: 50%; transform: translateY(-50%);"
></div>
<div
class="bg-blue-500 w-[20px] h-[20px] rounded-full absolute"
title={`pay: ${ranges[1].toLocaleString('en-GB', {
notation: 'compact',
currency: 'GBP',
style: 'currency'
})}`}
style="left: {10 +
scale(
ranges[1]
)}px; top: 50%; transform: translateY(-50%);"
></div>
{#if context.measureText(nameCompany).width < scale(ranges[1]) - scale(ranges[0]) - 40}
<div
class="absolute text-center text-white font-bold pb-1"
style="left: {10 +
scale(ranges[0]) +
10}px; width: {scale(ranges[1]) -
scale(ranges[0])}px;
top: 50%; transform: translateY(-50%); font-size: 10px; "
>
{nameCompany}
</div>
{:else}
<div
class="absolute text-center font-bold pb-1"
style="left: {10 +
scale(ranges[1] ?? ranges[0]) +
30}px;
top: 50%; transform: translateY(-50%); font-size: 10px; "
>
{nameCompany}
</div>
{/if}
</div>
{:else}
<div
class=" p-[10px] inset-2
{gotoIndex === index
? 'bg-purple-200/50'
: 'bg-slate-200/50'}
"
bind:this={indexPayRanges[index]}
>
<h2 class="font-bold">
{nameCompany} (Avg: {(
(ranges[0] + ranges[1]) /
2
).toLocaleString('en-GB', {
notation: 'compact',
currency: 'GBP',
style: 'currency'
})}; Min: {ranges[0].toLocaleString('en-GB', {
notation: 'compact',
currency: 'GBP',
style: 'currency'
})}; Max: {ranges[1].toLocaleString('en-GB', {
notation: 'compact',
currency: 'GBP',
style: 'currency'
})})
</h2>
{#each values as app}
<div
class="relative -mx-[10px] h-[40px]"
role="button"
tabindex={1}
onclick={() => {
applicationStore.loadItem = app as any;
goto('/');
}}
onkeydown={() => {
applicationStore.loadItem = app as any;
}}
>
{#if app.payrange[1]}
<div
class="bg-blue-500 w-[20px] h-[10px] rounded-full absolute"
style="left: {10 +
scale(app.payrange[0]) +
10}px; top: 50%; transform: translateY(-50%); width: {scale(
app.payrange[1]
) - scale(app.payrange[0])}px;"
></div>
{/if}
<div
class="bg-blue-500 w-[20px] h-[20px] rounded-full absolute"
title={`pay: ${app.payrange[0].toLocaleString(
'en-GB',
{
notation: 'compact',
currency: 'GBP',
style: 'currency'
}
)}`}
style="left: {10 +
scale(
app.payrange[0]
)}px; top: 50%; transform: translateY(-50%);"
></div>
{#if app.payrange[1]}
<div
class="bg-blue-500 w-[20px] h-[20px] rounded-full absolute"
title={`pay: ${app.payrange[1].toLocaleString(
'en-GB',
{
notation: 'compact',
currency: 'GBP',
style: 'currency'
}
)}`}
style="left: {10 +
scale(
app.payrange[1]
)}px; top: 50%; transform: translateY(-50%);"
></div>
{/if}
{#if context.measureText(app.title).width < scale(app.payrange[1]) - scale(app.payrange[0]) - 40}
<div
class="absolute text-center text-white font-bold pb-1"
style="left: {10 +
scale(app.payrange[0]) +
10}px; width: {scale(app.payrange[1]) -
scale(app.payrange[0])}px;
top: 50%; transform: translateY(-50%); font-size: 10px; "
>
{app.title}
</div>
{:else}
<div
class="absolute text-center font-bold pb-1"
style="left: {10 +
scale(
app.payrange[1] ?? app.payrange[0]
) +
30}px;
top: 50%; transform: translateY(-50%); font-size: 10px; "
>
{app.title}
</div>
{/if}
</div>
{/each}
</div>
{/if}
{/each}
{/if}
</div>
</div>
</div>
</div>
</div>
</HasUser>

View File

@@ -0,0 +1,172 @@
<script lang="ts">
import * as d3 from 'd3';
const {
title,
data,
sensitivity = 0.01,
width = 500,
height = 500,
dounout = false
}: {
title: string;
data: Record<string, number>;
sensitivity?: number;
width?: number;
height?: number;
dounout?: boolean;
} = $props();
let div: HTMLDivElement;
$effect(() => {
if (!div) return;
const labelsArray = Object.keys(data); //.toSorted((a, b) => data[a] - data[b]);
const valueArray = labelsArray.map((a) => data[a]);
const sum = valueArray.reduce((acc, d) => acc + d, 0);
let dataf = labelsArray
.map((l) => ({ name: l, value: data[l] }))
.filter((f) => f.value / sum > sensitivity);
const otherSum = valueArray.reduce((acc, v) => {
if (v / sum > sensitivity) return acc;
return acc + v;
}, 0);
if (otherSum > 0) {
dataf.push({
value: otherSum,
name: 'Other'
});
}
dataf = dataf.toSorted((a, b) => a.value - b.value);
const names = dataf.map((d) => d.name);
const values = dataf.map((d) => d.value);
const range = d3.range(names.length).filter((i) => !Number.isNaN(values[i]));
let colors: readonly string[] = [];
const groups = names;
const len = groups.length;
if (len === 0) {
// Do nothing
} else if (len === 1) {
colors = ['#FFCCC9'];
} else if (len === 2) {
colors = ['#FFCCC9', '#41EAD4'];
} else if (len < 10) {
colors = d3.schemeBlues[len];
} else {
colors = [...groups].map((_, i) => d3.interpolateBlues(i / len));
}
const color = d3.scaleOrdinal(groups, colors);
const innerRadius = 0; // inner radius of pie, in pixels (non-zero for donut)
const outerRadius = Math.min(width, height) / 2; // outer radius of pie, in pixels
const labelRadius = innerRadius * 0.2 + outerRadius * 0.8; // center radius of labels
const stroke = innerRadius > 0 ? 'none' : 'white'; // stroke separating widths
const strokeWidth = 1; // width of stroke separating wedges
const strokeLinejoin = 'round'; // line join of stroke separating wedges
const padAngle = stroke === 'none' ? 1 / outerRadius : 0; // angular separation between wedges, in radians
const arcs = d3
.pie()
.padAngle(padAngle)
.sort(null)
.value((i) => values[i as number])(range);
const arc = d3.arc().innerRadius(innerRadius).outerRadius(outerRadius);
const arcLabel = d3.arc().innerRadius(labelRadius).outerRadius(labelRadius);
const svg = d3
.create('svg')
.attr('width', width * 2)
.attr('height', height * 2)
.attr('viewBox', [-width, -height, width * 2, height * 2]);
const shapes = svg
.append('g')
.attr('stroke', stroke)
.attr('stroke-width', strokeWidth)
.attr('stroke-linejoin', strokeLinejoin);
const svg_arcs = shapes
.selectAll('path')
.data(arcs)
.join('path')
.attr('fill', (d, i) => color(`${names[d.data as number]}-${i}`))
.attr('d', arc as unknown as number);
svg_arcs
.append('title')
.text((d) => `${names[d.data as number]}: ${values[d.data as number]}`);
svg.append('g')
.attr('font-family', 'sans-serif')
.attr('text-anchor', 'middle')
.attr('font-size', 10)
.selectAll('text')
.data(arcs)
.join('text')
.attr('transform', (d) => `translate(${arcLabel.centroid(d as unknown as any)})`)
.selectAll('tspan')
.data((d) => {
if (d.endAngle - d.startAngle <= 0.2) {
return [];
}
if (d.endAngle - d.startAngle <= 0.25) {
return [{ t: 'title', text: names[d.data as number] }];
}
return [
{ t: 'title', text: names[d.data as number] },
{
t: 'value',
text: values[d.data as number].toLocaleString('en-GB', {
notation: 'compact'
})
}
];
})
.join('tspan')
.attr('x', 0)
.attr('y', (_, i) => `${i * 1.1}em`)
.attr('font-weight', (d) => (d.t === 'value' ? null : 'bold'))
.text((d) => {
return d.text;
});
if (dounout) {
const mask = svg.append('mask').attr('id', 'center-mask');
mask.append('circle')
.attr('cx', 0)
.attr('cy', 0)
.attr('fill', 'white')
.attr('r', width);
mask.append('circle')
.attr('cx', 0)
.attr('cy', 0)
.attr('fill', 'black')
.attr('r', width / 4);
shapes.attr('mask', 'url(#center-mask)');
}
div.innerHTML = '';
div.appendChild(svg.node() as unknown as Node);
});
</script>
<div>
{title}
<div class="relative m-auto overflow-hidden" style="width: {width}px; height: {height}px; ">
<div bind:this={div} style="transform: translate(-25%, -25%)" class="m-auto absolute"></div>
</div>
</div>

View File

@@ -51,7 +51,9 @@
</script>
{#if applicationStore.dragging && derivedItem}
<div class="flex w-full flex-grow rounded-lg p-3 gap-2 absolute bottom-0 left-0 right-0 bg-white">
<div
class="flex w-full flex-grow rounded-lg p-3 gap-2 absolute bottom-0 left-0 right-0 bg-white"
>
{#each statusStore.dirLinks[derivedItem.status_id] as node}
<DropZone icon={node.icon} ondrop={() => moveStatus(node.id, node.endable)}
>{node.name}</DropZone

View File

@@ -3,7 +3,8 @@
import { preventDefault } from '$lib/utils';
import type { Snippet } from 'svelte';
let { ondrop, icon, children }: { ondrop: () => void, icon: string, children: Snippet } = $props();
let { ondrop, icon, children }: { ondrop: () => void; icon: string; children: Snippet } =
$props();
</script>
<div
@@ -13,9 +14,7 @@
ondragenter={preventDefault(() => {})}
{ondrop}
>
<span
class="bi bi-{icon} text-7xl absolute"
class:animate-bounce={applicationStore.dragging}
<span class="bi bi-{icon} text-7xl absolute" class:animate-bounce={applicationStore.dragging}
></span>
<span class="bi bi-{icon} text-7xl opacity-0"></span>
<div class="text-xl">{@render children()}</div>

View File

@@ -13,7 +13,11 @@
onreload: (item: Application) => void;
} = $props();
let filter = $state('');
let filter = $state(application.company ? `@ ${application.company}` : '');
$effect(() => {
filter = application.company ? `@ ${application.company}` : '';
});
async function submit(item: Application) {
try {
@@ -29,6 +33,7 @@
let internal = $derived(
applicationStore.all.filter((i) => {
if (i.id === application.id) return false;
if (!filter) {
return true;
}
@@ -46,9 +51,9 @@
</script>
<dialog class="card max-w-[50vw]" bind:this={dialog}>
<div class="flex">
<div class="flex items-center">
<input placeholder="Filter" class="p-2 flex-grow" bind:value={filter} />
<div>
<div class="p-2">
{internal.length}
</div>
</div>

View File

@@ -83,7 +83,18 @@
<form bind:this={form} onsubmit={preventDefault(submit)}>
<fieldset>
<label class="flabel" for="text">Url</label>
<textarea class="finput min-w-96 min-h-96" id="text" bind:value={data.url}></textarea>
<textarea
class="finput min-w-96 min-h-96"
id="text"
bind:value={data.url}
onkeydown={(e) => {
if (e.key === 'Enter' && data.url === '' && hasExtension) {
e.preventDefault();
askForUrl();
return;
}
}}
></textarea>
</fieldset>
<div class="btns">
<button type="submit" class="btn-confirm">Update</button>

View File

@@ -60,6 +60,12 @@
lastEvent = event;
}
let d2 = new Date().getTime();
if (_events.length > 0) {
let d1 = new Date(_events[_events.length - 1].time).getTime();
_events[_events.length - 1].timeDiff = calcDiff(d2 - d1);
}
// Todo endable
/*
if (_events.length > 0 && !endable.includes(_events[_events.length - 1].new_status_id)) {
@@ -85,10 +91,14 @@
class="shadow-sm shadow-violet-500 border rounded-full min-w-[50px] min-h-[50px] grid place-items-center bg-white z-10"
>
{#if event.event_type == EventType.Creation}
<span title={`Created @\n ${new Date(event.time).toLocaleString()}`} class="bi bi-plus"
<span
title={`Created @\n ${new Date(event.time).toLocaleString()}`}
class="bi bi-plus"
></span>
{:else if event.event_type == EventType.View}
<span title={`Viewed @\n ${new Date(event.time).toLocaleString()}`} class="bi bi-eye"
<span
title={`Viewed @\n ${new Date(event.time).toLocaleString()}`}
class="bi bi-eye"
></span>
{:else}
<span
@@ -102,7 +112,9 @@
<!-- TODO -->
<!-- || !endable.includes(event.new_status) -->
{#if i != events.length - 1 || !statusStore.nodesR[event.new_status_id].endable}
<div class="min-w-[70px] h-[18px] bg-blue-500 -mx-[10px] px-[20px] flex-grow text-center">
<div
class="min-w-[70px] h-[18px] bg-blue-500 -mx-[10px] px-[20px] flex-grow text-center"
>
{#if event.timeDiff}
<div class="-mt-[3px] text-white">
<span class="bi bi-clock"></span>

View File

@@ -14,7 +14,6 @@
import CompanyField from './CompanyField.svelte';
import AutoDropZone from './AutoDropZone.svelte';
import { statusStore } from '$lib/Types.svelte';
import { thresholdFreedmanDiaconis } from 'd3';
// Not this represents the index in the store array
let activeItem: number | undefined = $state();
@@ -124,9 +123,27 @@
function setExtData() {
if (!lastExtData || activeItem === undefined || !derivedItem) return;
applicationStore.all[activeItem].title = lastExtData.jobTitle;
applicationStore.all[activeItem].company = lastExtData.company;
applicationStore.all[activeItem].title = lastExtData.jobTitle.replace(/\&amp;/, '&');
applicationStore.all[activeItem].company = lastExtData.company.replace(/\&amp;/, '&');
applicationStore.all[activeItem].payrange = lastExtData.money;
const title: string = lastExtData.jobTitle;
if (title.match(/intern|apprenticeship/i)) {
applicationStore.all[activeItem].job_level = 'intern';
} else if (title.match(/graduate/i)) {
applicationStore.all[activeItem].job_level = 'entry';
} else if (title.match(/junior|associate/i)) {
applicationStore.all[activeItem].job_level = 'junior';
} else if (title.match(/mid/i)) {
applicationStore.all[activeItem].job_level = 'mid';
} else if (title.match(/senior|III/i)) {
applicationStore.all[activeItem].job_level = 'senior';
} else if (title.match(/staff/i)) {
applicationStore.all[activeItem].job_level = 'staff';
} else if (title.match(/lead/i)) {
applicationStore.all[activeItem].job_level = 'lead';
}
window.requestAnimationFrame(() => {
save().then(async () => {
if (activeItem === undefined) return;
@@ -274,6 +291,23 @@
</div>
</fieldset>
{/if}
<fieldset>
<label class="flabel" for="title">Job Level</label>
<select
class="finput"
id="job_level"
bind:value={applicationStore.all[activeItem].job_level}
onchange={save}
>
<option value="intern"> Intern </option>
<option value="entry"> Entry </option>
<option value="junior"> Junior </option>
<option value="mid"> Mid </option>
<option value="senior"> Senior </option>
<option value="staff"> Staff </option>
<option value="lead"> Lead </option>
</select>
</fieldset>
<div>
<div class="flabel">Tags</div>
<div class="flex gap-2 flex-wrap">