feat: improve search in the linking
This commit is contained in:
parent
c8e45febfc
commit
54e26f170a
210
site/src/lib/ApplicationSearchBar.svelte
Normal file
210
site/src/lib/ApplicationSearchBar.svelte
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { applicationStore, type Application } from '$lib/ApplicationsStore.svelte';
|
||||||
|
import InplaceDialog from '$lib/InplaceDialog.svelte';
|
||||||
|
import { statusStore } from '$lib/Types.svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
excludeApplication,
|
||||||
|
result = $bindable([]),
|
||||||
|
filter = $bindable('')
|
||||||
|
}: {
|
||||||
|
excludeApplication?: Application;
|
||||||
|
result: Application[];
|
||||||
|
filter?: string;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const JobLevels = ['intern', 'entry', 'junior', 'mid', 'senior', 'staff', 'lead', 'none'];
|
||||||
|
|
||||||
|
let filterStatus: string[] = $state([]);
|
||||||
|
let jobLevels: string[] = $state([]);
|
||||||
|
let advFilters = $state(false);
|
||||||
|
|
||||||
|
type ExtraFilterType =
|
||||||
|
| {
|
||||||
|
type: 'name';
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
| { type: 'company'; text: string }
|
||||||
|
| { type: 'query'; text: string };
|
||||||
|
|
||||||
|
let extraFiltersToDisplay: ExtraFilterType[] = $state([]);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!filter) {
|
||||||
|
extraFiltersToDisplay = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
result = applicationStore.all.filter((i) => {
|
||||||
|
if (i.linked_application) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (excludeApplication && i.id == excludeApplication.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterStatus.length !== 0 && !filterStatus.includes(i.status_id ?? '')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const temp = jobLevels.map((a) => (a === 'none' ? '' : a));
|
||||||
|
if (temp.length !== 0 && !temp.includes(i.job_level ?? '')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filter) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.includes('@') && i.company) {
|
||||||
|
const splits = filter.split('@');
|
||||||
|
|
||||||
|
const newExtraFilters: ExtraFilterType[] = [];
|
||||||
|
const name = splits[0].trim();
|
||||||
|
const company = splits[1].trim();
|
||||||
|
if (name.length !== 0) {
|
||||||
|
newExtraFilters.push({
|
||||||
|
type: 'name',
|
||||||
|
text: name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (company.length !== 0) {
|
||||||
|
newExtraFilters.push({
|
||||||
|
type: 'company',
|
||||||
|
text: company
|
||||||
|
});
|
||||||
|
}
|
||||||
|
extraFiltersToDisplay = newExtraFilters;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const f = new RegExp(name, 'ig');
|
||||||
|
const c = new RegExp(company, 'ig');
|
||||||
|
return i.title.match(f) && i.company.match(c);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extraFiltersToDisplay = [{ type: 'query', text: filter }];
|
||||||
|
try {
|
||||||
|
const f = new RegExp(filter, 'ig');
|
||||||
|
let x = i.title;
|
||||||
|
|
||||||
|
if (i.company) {
|
||||||
|
x = `${x} @ ${i.company}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.match(f);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex sticky top-0 bg-white z-50 p-2 shadow-lg rounded-lg gap-2 flex-col">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input placeholder="Filter" class="p-2 flex-grow" bind:value={filter} />
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
advFilters = !advFilters;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="bi bi-filter"></span>
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
{result.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if advFilters}
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<InplaceDialog
|
||||||
|
buttonClass="border-slate-300 border border-solid color-slate-300 p-1 rounded-md bg-slate-100/50"
|
||||||
|
dialogClass="w-[200px]"
|
||||||
|
>
|
||||||
|
{#snippet buttonChildren()}
|
||||||
|
<i class="bi bi-plus"></i> Status
|
||||||
|
{/snippet}
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#each statusStore.nodes.filter((a) => !filterStatus.includes(a.id)) as node}
|
||||||
|
<button
|
||||||
|
class="border-violet-300 border border-solid color-violet-300 bg-violet-100/50 p-1 rounded-md text-violet-800"
|
||||||
|
onclick={() => {
|
||||||
|
//filterStatus.push(node.id);
|
||||||
|
//filterStatus = filterStatus;
|
||||||
|
filterStatus = [...filterStatus, node.id];
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{node.name}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</InplaceDialog>
|
||||||
|
<InplaceDialog
|
||||||
|
buttonClass="border-slate-300 border border-solid color-slate-300 p-1 rounded-md bg-slate-100/50"
|
||||||
|
dialogClass="w-[200px]"
|
||||||
|
>
|
||||||
|
{#snippet buttonChildren()}
|
||||||
|
<i class="bi bi-plus"></i> Job Level
|
||||||
|
{/snippet}
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#each JobLevels.filter((a) => !jobLevels.includes(a)) as jobLevel}
|
||||||
|
<button
|
||||||
|
class="border-red-300 border border-solid color-red-300 bg-red-100/50 p-1 rounded-md text-red-800"
|
||||||
|
onclick={() => {
|
||||||
|
jobLevels = [...jobLevels, jobLevel];
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{jobLevel}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</InplaceDialog>
|
||||||
|
</div>
|
||||||
|
<h2>Filters</h2>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
{#each statusStore.nodes.filter((a) => filterStatus.includes(a.id)) as node}
|
||||||
|
<button
|
||||||
|
class="border-violet-300 border border-solid color-violet-300 bg-violet-100/50 text-violet-800 p-1 rounded-md"
|
||||||
|
onclick={() => {
|
||||||
|
filterStatus = filterStatus.filter((a) => a != node.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{node.name}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{#each jobLevels.filter((a) => jobLevels.includes(a)) as jobLevel}
|
||||||
|
<button
|
||||||
|
class="border-red-300 border border-solid color-red-300 bg-red-100/50 p-1 rounded-md text-red-800"
|
||||||
|
onclick={() => {
|
||||||
|
jobLevels = jobLevels.filter((a) => a != jobLevel);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{jobLevel}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{#each extraFiltersToDisplay as filter}
|
||||||
|
{#if filter.type === 'name'}
|
||||||
|
<span
|
||||||
|
class="border-blue-300 border border-solid color-blue-300 bg-blue-100/50 p-1 rounded-md"
|
||||||
|
>
|
||||||
|
Name ~ /<span class="text-blue-800 text-bold">{filter.text}</span>/
|
||||||
|
</span>
|
||||||
|
{:else if filter.type === 'company'}
|
||||||
|
<span
|
||||||
|
class="border-green-300 border border-solid color-green-300 bg-green-100/50 p-1 rounded-md"
|
||||||
|
>
|
||||||
|
Company ~ /<span class="text-green-800 text-bold">{filter.text}</span>/
|
||||||
|
</span>
|
||||||
|
{:else if filter.type === 'query'}
|
||||||
|
<span
|
||||||
|
class="border-orange-300 border border-solid color-orange-300 bg-orange-100/50 p-1 rounded-md"
|
||||||
|
>
|
||||||
|
Query ~ /<span class="text-orange-800 text-bold">{filter.text}</span>/
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import ApplicationSearchBar from '$lib/ApplicationSearchBar.svelte';
|
||||||
import { applicationStore, type Application } from '$lib/ApplicationsStore.svelte';
|
import { applicationStore, type Application } from '$lib/ApplicationsStore.svelte';
|
||||||
import { statusStore } from '$lib/Types.svelte';
|
import { statusStore } from '$lib/Types.svelte';
|
||||||
import { post } from '$lib/utils';
|
import { post } from '$lib/utils';
|
||||||
@ -31,32 +32,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let internal = $derived(
|
let internal: Application[] = $state([]);
|
||||||
applicationStore.all.filter((i) => {
|
|
||||||
if (i.id === application.id) return false;
|
|
||||||
if (!filter) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const f = new RegExp(filter, 'ig');
|
|
||||||
|
|
||||||
let x = i.title;
|
|
||||||
|
|
||||||
if (i.company) {
|
|
||||||
x = `${x} @ ${i.company}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return x.match(f);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<dialog class="card max-w-[50vw]" bind:this={dialog}>
|
<dialog class="card max-w-[50vw]" bind:this={dialog}>
|
||||||
<div class="flex items-center">
|
<ApplicationSearchBar bind:result={internal} bind:filter />
|
||||||
<input placeholder="Filter" class="p-2 flex-grow" bind:value={filter} />
|
|
||||||
<div class="p-2">
|
|
||||||
{internal.length}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-y-auto overflow-x-hidden flex-grow p-2">
|
<div class="overflow-y-auto overflow-x-hidden flex-grow p-2">
|
||||||
{#each internal as item}
|
{#each internal as item}
|
||||||
<div class="card p-2 my-2 bg-slate-100 max-w-full" role="none">
|
<div class="card p-2 my-2 bg-slate-100 max-w-full" role="none">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { applicationStore, type Application } from '$lib/ApplicationsStore.svelte';
|
import ApplicationSearchBar from '$lib/ApplicationSearchBar.svelte';
|
||||||
import InplaceDialog from '$lib/InplaceDialog.svelte';
|
import type { Application } from '$lib/ApplicationsStore.svelte';
|
||||||
import { statusStore } from '$lib/Types.svelte';
|
import { statusStore } from '$lib/Types.svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@ -11,31 +11,8 @@
|
|||||||
onreload: (item: Application) => void;
|
onreload: (item: Application) => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const JobLevels = ['intern', 'entry', 'junior', 'mid', 'senior', 'staff', 'lead', 'none'];
|
|
||||||
|
|
||||||
let filter = $state('');
|
|
||||||
let filterStatus: string[] = $state([]);
|
|
||||||
let jobLevels: string[] = $state([]);
|
|
||||||
let advFilters = $state(false);
|
|
||||||
|
|
||||||
type ExtraFilterType =
|
|
||||||
| {
|
|
||||||
type: 'name';
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
| { type: 'company'; text: string }
|
|
||||||
| { type: 'query'; text: string };
|
|
||||||
|
|
||||||
let extraFiltersToDisplay: ExtraFilterType[] = $state([]);
|
|
||||||
|
|
||||||
let dialogElement: HTMLDialogElement;
|
let dialogElement: HTMLDialogElement;
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (!filter) {
|
|
||||||
extraFiltersToDisplay = [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function docKey(e: KeyboardEvent) {
|
function docKey(e: KeyboardEvent) {
|
||||||
if (e.ctrlKey && e.code === 'KeyK') {
|
if (e.ctrlKey && e.code === 'KeyK') {
|
||||||
dialogElement.showModal();
|
dialogElement.showModal();
|
||||||
@ -52,180 +29,11 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
let internal = $derived(
|
let internal: Application[] = $state([]);
|
||||||
applicationStore.all.filter((i) => {
|
|
||||||
if (i.linked_application) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (application && i.id == application.id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterStatus.length !== 0 && !filterStatus.includes(i.status_id ?? '')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const temp = jobLevels.map((a) => (a === 'none' ? '' : a));
|
|
||||||
if (temp.length !== 0 && !temp.includes(i.job_level ?? '')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!filter) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.includes('@') && i.company) {
|
|
||||||
const splits = filter.split('@');
|
|
||||||
|
|
||||||
const newExtraFilters: ExtraFilterType[] = [];
|
|
||||||
const name = splits[0].trim();
|
|
||||||
const company = splits[1].trim();
|
|
||||||
if (name.length !== 0) {
|
|
||||||
newExtraFilters.push({
|
|
||||||
type: 'name',
|
|
||||||
text: name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (company.length !== 0) {
|
|
||||||
newExtraFilters.push({
|
|
||||||
type: 'company',
|
|
||||||
text: company
|
|
||||||
});
|
|
||||||
}
|
|
||||||
extraFiltersToDisplay = newExtraFilters;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const f = new RegExp(name, 'ig');
|
|
||||||
const c = new RegExp(company, 'ig');
|
|
||||||
return i.title.match(f) && i.company.match(c);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extraFiltersToDisplay = [{ type: 'query', text: filter }];
|
|
||||||
try {
|
|
||||||
const f = new RegExp(filter, 'ig');
|
|
||||||
let x = i.title;
|
|
||||||
|
|
||||||
if (i.company) {
|
|
||||||
x = `${x} @ ${i.company}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return x.match(f);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<dialog class="card max-w-[50vw]" bind:this={dialogElement}>
|
<dialog class="card max-w-[50vw]" bind:this={dialogElement}>
|
||||||
<div class="flex sticky top-0 bg-white z-50 p-2 shadow-lg rounded-lg gap-2 flex-col">
|
<ApplicationSearchBar bind:result={internal} excludeApplication={application} />
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<input placeholder="Filter" class="p-2 flex-grow" bind:value={filter} />
|
|
||||||
<button
|
|
||||||
onclick={() => {
|
|
||||||
advFilters = !advFilters;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span class="bi bi-filter"></span>
|
|
||||||
</button>
|
|
||||||
<div>
|
|
||||||
{internal.length}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if advFilters}
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<InplaceDialog
|
|
||||||
buttonClass="border-slate-300 border border-solid color-slate-300 p-1 rounded-md bg-slate-100/50"
|
|
||||||
dialogClass="w-[200px]"
|
|
||||||
>
|
|
||||||
{#snippet buttonChildren()}
|
|
||||||
<i class="bi bi-plus"></i> Status
|
|
||||||
{/snippet}
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
{#each statusStore.nodes.filter((a) => !filterStatus.includes(a.id)) as node}
|
|
||||||
<button
|
|
||||||
class="border-violet-300 border border-solid color-violet-300 bg-violet-100/50 p-1 rounded-md text-violet-800"
|
|
||||||
onclick={() => {
|
|
||||||
//filterStatus.push(node.id);
|
|
||||||
//filterStatus = filterStatus;
|
|
||||||
filterStatus = [...filterStatus, node.id];
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{node.name}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</InplaceDialog>
|
|
||||||
<InplaceDialog
|
|
||||||
buttonClass="border-slate-300 border border-solid color-slate-300 p-1 rounded-md bg-slate-100/50"
|
|
||||||
dialogClass="w-[200px]"
|
|
||||||
>
|
|
||||||
{#snippet buttonChildren()}
|
|
||||||
<i class="bi bi-plus"></i> Job Level
|
|
||||||
{/snippet}
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
{#each JobLevels.filter((a) => !jobLevels.includes(a)) as jobLevel}
|
|
||||||
<button
|
|
||||||
class="border-red-300 border border-solid color-red-300 bg-red-100/50 p-1 rounded-md text-red-800"
|
|
||||||
onclick={() => {
|
|
||||||
jobLevels = [...jobLevels, jobLevel];
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{jobLevel}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</InplaceDialog>
|
|
||||||
</div>
|
|
||||||
<h2>Filters</h2>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
{#each statusStore.nodes.filter((a) => filterStatus.includes(a.id)) as node}
|
|
||||||
<button
|
|
||||||
class="border-violet-300 border border-solid color-violet-300 bg-violet-100/50 text-violet-800 p-1 rounded-md"
|
|
||||||
onclick={() => {
|
|
||||||
filterStatus = filterStatus.filter((a) => a != node.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{node.name}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
{#each jobLevels.filter((a) => jobLevels.includes(a)) as jobLevel}
|
|
||||||
<button
|
|
||||||
class="border-red-300 border border-solid color-red-300 bg-red-100/50 p-1 rounded-md text-red-800"
|
|
||||||
onclick={() => {
|
|
||||||
jobLevels = jobLevels.filter((a) => a != jobLevel);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{jobLevel}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
{#each extraFiltersToDisplay as filter}
|
|
||||||
{#if filter.type === 'name'}
|
|
||||||
<span
|
|
||||||
class="border-blue-300 border border-solid color-blue-300 bg-blue-100/50 p-1 rounded-md"
|
|
||||||
>
|
|
||||||
Name ~ /<span class="text-blue-800 text-bold">{filter.text}</span>/
|
|
||||||
</span>
|
|
||||||
{:else if filter.type === 'company'}
|
|
||||||
<span
|
|
||||||
class="border-green-300 border border-solid color-green-300 bg-green-100/50 p-1 rounded-md"
|
|
||||||
>
|
|
||||||
Company ~ /<span class="text-green-800 text-bold">{filter.text}</span>/
|
|
||||||
</span>
|
|
||||||
{:else if filter.type === 'query'}
|
|
||||||
<span
|
|
||||||
class="border-orange-300 border border-solid color-orange-300 bg-orange-100/50 p-1 rounded-md"
|
|
||||||
>
|
|
||||||
Query ~ /<span class="text-orange-800 text-bold">{filter.text}</span>/
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div class="overflow-y-auto overflow-x-hidden flex-grow p-2">
|
<div class="overflow-y-auto overflow-x-hidden flex-grow p-2">
|
||||||
<!-- TODO loading screen -->
|
<!-- TODO loading screen -->
|
||||||
{#each internal as item}
|
{#each internal as item}
|
||||||
|
Loading…
Reference in New Issue
Block a user