This commit is contained in:
Hadi
2026-04-06 15:12:34 +02:00
commit 4989225671
117 changed files with 11454 additions and 0 deletions

View File

@@ -0,0 +1,232 @@
<script>
import { Search, AlertTriangle } from "@lucide/svelte";
import Select from "./comps/Select.svelte";
import { INPUT_TYPES } from "@src/lib/vars";
let { onSearch = async () => {}, demo = false } = $props();
const DETECTORS = {
email: (_raw, v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
phone: (_raw, v) => /^\+\d{1,4} \d{4,}$/.test(v),
ip: (_raw, v) => /^(\d{1,3}\.){3}\d{1,3}$/.test(v) || /^[0-9a-fA-F:]{3,39}$/.test(v),
domain: (raw, v) => /^https?:\/\//.test(raw) || /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(v),
name: (_raw, v) => /^[a-zA--ÿ'-]+(?: [a-zA-ZÀ-ÿ'-]+){1,2}$/.test(v),
};
const VALIDATORS = {
email: { test: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), msg: "Invalid email address" },
username: { test: (v) => /^[a-zA-Z0-9._-]+$/.test(v), msg: "Username may only contain a-z, 0-9, . - _" },
phone: { test: (v) => /^\+\d{1,4} \d{4,}$/.test(v), msg: "Format: +INDICATIF NUMERO (ex: +33 0612345678)" },
ip: { test: (v) => /^(\d{1,3}\.){3}\d{1,3}$/.test(v) || /^[0-9a-fA-F:]{3,39}$/.test(v), msg: "Invalid IP address" },
domain: { test: (v) => /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(v), msg: "Invalid domain name" },
};
let target = $state("");
let inputType = $state("email");
let profile = $state("default");
let profiles = $state([]);
let loading = $state(false);
let error = $state("");
let validationError = $state("");
// null = auto-switch free; "TYPE" = auto-switched from TYPE (show revert); "__locked__" = user overrode, no auto-switch
let prevType = $state(null);
let showRevert = $derived(prevType !== null && prevType !== "__locked__");
let profileOptions = $derived(profiles.map((p) => p.name));
let strippedTarget = $derived.by(() => {
let v = target.trim();
v = v.replace(/^https?:\/\//, "");
if (v.startsWith("@")) v = v.slice(1);
return v;
});
let detectedType = $derived.by(() => {
const raw = target.trim();
const v = strippedTarget;
if (!v && !raw) return null;
if (raw.startsWith("@")) return "username";
for (const [type, fn] of Object.entries(DETECTORS)) {
if (fn(raw, v)) return type;
}
return null;
});
async function loadProfiles() {
try {
const res = await fetch("/api/config/profiles");
if (res.ok) profiles = await res.json();
} catch (_) {}
}
loadProfiles();
function sanitize(s) {
return s.replace(/[<>"'`&]/g, "").trim();
}
function validate(val, type) {
const v = VALIDATORS[type];
if (!v) return "";
return v.test(val) ? "" : v.msg;
}
function onTargetInput() {
if (!strippedTarget) prevType = null; // reset when field is cleared
if (validationError) validationError = validate(strippedTarget, inputType);
if (prevType === null && detectedType && detectedType !== inputType) {
prevType = inputType;
inputType = detectedType;
}
}
function revert() {
inputType = prevType;
prevType = "__locked__";
validationError = "";
}
function onSelectType(v) {
inputType = v;
prevType = "__locked__";
validationError = "";
}
async function submit() {
if (demo) return;
const clean = sanitize(strippedTarget);
if (!clean) return;
validationError = validate(clean, inputType);
if (validationError) return;
error = "";
loading = true;
try {
await onSearch(clean, inputType, profile);
target = "";
prevType = null;
} catch (e) {
error = e.message;
} finally {
loading = false;
}
}
</script>
<div class="flex flex-col gap-2">
{#if error}
<div class="alert alert-error text-sm py-2 gap-2"><AlertTriangle size={15} class="shrink-0" />{error}</div>
{/if}
{#if showRevert}
<div class="flex items-center gap-2 px-1 text-xs text-base-content/50">
<span>Switched to <span class="text-base-content/70 font-medium">{inputType}</span></span>
<button
class="badge badge-ghost badge-sm hover:badge-primary transition-colors cursor-pointer"
onclick={revert}
>
{prevType}
</button>
</div>
{/if}
<!-- Mobile layout -->
<div class="flex flex-col gap-2 sm:hidden">
<div class="flex items-center rounded-xl border border-base-content/15 bg-base-300
focus-within:border-primary/40 transition-colors">
<input
class="flex-1 bg-transparent px-4 py-3 outline-none text-sm
placeholder:text-base-content/30 min-w-0"
placeholder={demo ? "Search disabled in demo mode" : "Enter target..."}
bind:value={target}
oninput={onTargetInput}
onkeydown={(e) => e.key === "Enter" && submit()}
disabled={demo}
/>
</div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-1">
<span class="text-xs text-base-content/25 pl-1">type</span>
<Select
options={INPUT_TYPES}
selected={inputType}
size="xs"
onselect={onSelectType}
/>
</div>
<div class="flex items-center gap-1">
<span class="text-xs text-base-content/25">profile</span>
<Select
options={profileOptions}
selected={profile}
size="xs"
onselect={(v) => { profile = v; }}
/>
</div>
<button
class="btn btn-primary btn-sm flex-1 gap-1"
onclick={submit}
disabled={demo || loading || !target.trim()}
>
{#if loading}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<Search size={14} />
{/if}
Search
</button>
</div>
</div>
<!-- Desktop layout -->
<div
class="hidden sm:flex items-center rounded-xl border border-base-content/15 bg-base-300
focus-within:border-primary/40 transition-colors"
>
<div class="border-r border-base-content/10 flex items-center gap-1 pl-3">
<span class="text-xs text-base-content/25 shrink-0">type</span>
<Select
options={INPUT_TYPES}
selected={inputType}
onselect={onSelectType}
/>
</div>
<input
class="flex-1 bg-transparent px-4 py-2.5 outline-none text-sm
placeholder:text-base-content/30 min-w-0"
placeholder={demo ? "Search disabled in demo mode" : "Enter target..."}
bind:value={target}
oninput={onTargetInput}
onkeydown={(e) => e.key === "Enter" && submit()}
disabled={demo}
/>
<div class="border-l border-base-content/10 flex items-center gap-1 pr-1 pl-3">
<span class="text-xs text-base-content/25 shrink-0">profile</span>
<Select
options={profileOptions}
selected={profile}
onselect={(v) => { profile = v; }}
/>
</div>
<button
class="btn btn-primary btn-sm m-1 rounded-lg gap-1 shrink-0"
onclick={submit}
disabled={demo || loading || !target.trim()}
>
{#if loading}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<Search size={14} />
{/if}
Search
</button>
</div>
{#if validationError}
<p class="text-xs text-error pl-1">{validationError}</p>
{/if}
</div>