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}