From 54e26f170aaa1a18b145b0230a31e65558510611 Mon Sep 17 00:00:00 2001 From: Andre Henriques <andr3h3nriqu3s@gmail.com> Date: Fri, 4 Apr 2025 11:33:08 +0100 Subject: [PATCH] feat: improve search in the linking --- site/src/lib/ApplicationSearchBar.svelte | 210 ++++++++++++++++++ .../routes/work-area/LinkApplication.svelte | 26 +-- .../routes/work-area/SearchApplication.svelte | 200 +---------------- 3 files changed, 217 insertions(+), 219 deletions(-) create mode 100644 site/src/lib/ApplicationSearchBar.svelte diff --git a/site/src/lib/ApplicationSearchBar.svelte b/site/src/lib/ApplicationSearchBar.svelte new file mode 100644 index 0000000..fc28c62 --- /dev/null +++ b/site/src/lib/ApplicationSearchBar.svelte @@ -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> diff --git a/site/src/routes/work-area/LinkApplication.svelte b/site/src/routes/work-area/LinkApplication.svelte index a2df991..415a33f 100644 --- a/site/src/routes/work-area/LinkApplication.svelte +++ b/site/src/routes/work-area/LinkApplication.svelte @@ -1,4 +1,5 @@ <script lang="ts"> + import ApplicationSearchBar from '$lib/ApplicationSearchBar.svelte'; import { applicationStore, type Application } from '$lib/ApplicationsStore.svelte'; import { statusStore } from '$lib/Types.svelte'; import { post } from '$lib/utils'; @@ -31,32 +32,11 @@ } } - let internal = $derived( - 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); - }) - ); + let internal: Application[] = $state([]); </script> <dialog class="card max-w-[50vw]" bind:this={dialog}> - <div class="flex items-center"> - <input placeholder="Filter" class="p-2 flex-grow" bind:value={filter} /> - <div class="p-2"> - {internal.length} - </div> - </div> + <ApplicationSearchBar bind:result={internal} bind:filter /> <div class="overflow-y-auto overflow-x-hidden flex-grow p-2"> {#each internal as item} <div class="card p-2 my-2 bg-slate-100 max-w-full" role="none"> diff --git a/site/src/routes/work-area/SearchApplication.svelte b/site/src/routes/work-area/SearchApplication.svelte index 85a1444..83bb91d 100644 --- a/site/src/routes/work-area/SearchApplication.svelte +++ b/site/src/routes/work-area/SearchApplication.svelte @@ -1,6 +1,6 @@ <script lang="ts"> - import { applicationStore, type Application } from '$lib/ApplicationsStore.svelte'; - import InplaceDialog from '$lib/InplaceDialog.svelte'; + import ApplicationSearchBar from '$lib/ApplicationSearchBar.svelte'; + import type { Application } from '$lib/ApplicationsStore.svelte'; import { statusStore } from '$lib/Types.svelte'; let { @@ -11,31 +11,8 @@ onreload: (item: Application) => void; } = $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; - $effect(() => { - if (!filter) { - extraFiltersToDisplay = []; - } - }); - function docKey(e: KeyboardEvent) { if (e.ctrlKey && e.code === 'KeyK') { dialogElement.showModal(); @@ -52,180 +29,11 @@ }; }); - let internal = $derived( - 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; - } - }) - ); + let internal: Application[] = $state([]); </script> <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"> - <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> + <ApplicationSearchBar bind:result={internal} excludeApplication={application} /> <div class="overflow-y-auto overflow-x-hidden flex-grow p-2"> <!-- TODO loading screen --> {#each internal as item}