OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
 
master @ 427 LINES
 
[ HISTORY ]  [ UP ]
 

import Fuse from "fuse.js";
import type { FuseResult } from "fuse.js";
import { buildPlayerProfileURL, debounce } from "../../lib/utils.ts";
import { getClassColor } from "../../lib/client-utils.ts";

type PlayerSearchResult = {
  id: number;
  name: string;
  realm_slug: string;
  realm_name: string;
  region: string;
  class_name?: string;
  active_spec_name?: string;
  global_ranking?: number;
};
type PlayerSearchIndex = { players: PlayerSearchResult[]; metadata: any };

class PlayerSearchClient {
  private container: HTMLElement;
  private searchInput: HTMLInputElement;
  private clearButton: HTMLButtonElement;
  private resultsContainer: HTMLElement;
  private resultsList: HTMLElement;
  private loadingDiv: HTMLElement;
  private noResultsDiv: HTMLElement;
  private playerIndex: PlayerSearchResult[] = [];
  private fuse: Fuse<PlayerSearchResult> | null = null;
  private isIndexLoaded = false;
  private selectedIndex = -1;
  private filteredResults: FuseResult<PlayerSearchResult>[] = [];
  private totalExpected = 0;

  constructor(container: HTMLElement) {
    this.container = container;
    this.searchInput = container.querySelector(
      "#player-search-input",
    ) as HTMLInputElement;
    this.clearButton = container.querySelector(
      "#search-clear",
    ) as HTMLButtonElement;
    this.resultsContainer = container.querySelector(
      "#search-results",
    ) as HTMLElement;
    this.resultsList = container.querySelector(
      "#search-results-list",
    ) as HTMLElement;
    this.loadingDiv = container.querySelector("#search-loading") as HTMLElement;
    this.noResultsDiv = container.querySelector(
      "#search-no-results",
    ) as HTMLElement;

    this.bindEvents();
    this.bindGlobalHotkeys();
  }

  private bindEvents() {
    const debouncedSearch = debounce((q: string) => this.performSearch(q), 300);
    this.searchInput.addEventListener("input", (e) => {
      if (!this.isIndexLoaded) this.loadPlayerIndex(1000);
      const q = (e.target as HTMLInputElement).value;
      this.updateClearButton(q);
      debouncedSearch(q);
    });
    this.searchInput.addEventListener("focus", () => {
      if (!this.isIndexLoaded) this.loadPlayerIndex(1000);
      if (this.searchInput.value.length > 0) this.showResults();
    });
    this.clearButton.addEventListener("click", () => this.clearSearch());
    this.searchInput.addEventListener("keydown", (e) => this.handleKeyboard(e));
    document.addEventListener("click", (e) => {
      if (!this.container.contains(e.target as Node)) this.hideResults();
    });
  }

  private bindGlobalHotkeys() {
    document.addEventListener("keydown", (e) => {
      if (e.key === "/" && !e.ctrlKey && !e.metaKey && !e.altKey) {
        const tag =
          (document.activeElement &&
            (document.activeElement as HTMLElement).tagName) ||
          "";
        if (tag !== "INPUT" && tag !== "TEXTAREA") {
          e.preventDefault();
          this.searchInput.focus();
          this.searchInput.value = "";
          this.showResults();
        }
      }
    });
  }

  private updateClearButton(q: string) {
    this.clearButton.style.display = q.length > 0 ? "block" : "none";
  }

  private clearSearch() {
    this.searchInput.value = "";
    this.clearButton.style.display = "none";
    this.hideResults();
    this.searchInput.focus();
  }

  private showResults() {
    this.resultsContainer.style.display = "block";
    this.noResultsDiv.style.display = "none";
    this.resultsList.style.display = "block";
  }
  private hideResults() {
    this.resultsContainer.style.display = "none";
    this.selectedIndex = -1;
  }
  private showLoading() {
    this.resultsContainer.style.display = "block";
    this.loadingDiv.style.display = "flex";
    this.resultsList.style.display = "none";
    this.noResultsDiv.style.display = "none";
  }
  private hideLoading() {
    this.loadingDiv.style.display = "none";
  }
  private showNoResults() {
    this.resultsContainer.style.display = "block";
    this.noResultsDiv.style.display = "block";
    this.resultsList.style.display = "none";
    this.loadingDiv.style.display = "none";
  }

  private loadFromCache(): PlayerSearchIndex | null {
    try {
      const cached = localStorage.getItem("playerSearchIndex");
      if (!cached) return null;
      const data = JSON.parse(cached);
      if (!data?.players || data.players.length === 0) {
        console.log("[PlayerSearch] cache present but empty; ignoring");
        localStorage.removeItem("playerSearchIndex");
        return null;
      }
      const age = Date.now() - new Date(data.metadata.cached_at).getTime();
      if (age < 24 * 60 * 60 * 1000) return data;
      localStorage.removeItem("playerSearchIndex");
      return null;
    } catch {
      localStorage.removeItem("playerSearchIndex");
      return null;
    }
  }

  private saveToCache(data: PlayerSearchIndex) {
    try {
      const payload = {
        ...data,
        metadata: { ...data.metadata, cached_at: new Date().toISOString() },
      };
      localStorage.setItem("playerSearchIndex", JSON.stringify(payload));
    } catch {}
  }

  private async loadPlayerIndex(limit = 5000) {
    if (this.isIndexLoaded) return;
    const cached = this.loadFromCache();
    if (cached) {
      console.log("[PlayerSearch] using cached index:", cached.players.length);
      this.setupIndex(cached);
      return;
    }
    this.showLoading();
    try {
      // Fetch first shard to get total and initialize
      const firstUrl = `/api/search/players-000.json`;
      console.log("[PlayerSearch] fetching index:", firstUrl);
      const firstRes = await fetch(firstUrl);
      console.log(
        "[PlayerSearch] index response:",
        firstRes.status,
        firstRes.statusText,
      );
      if (!firstRes.ok)
        throw new Error(`${firstRes.status} ${firstRes.statusText}`);
      const firstData: PlayerSearchIndex = await firstRes.json();
      const firstPlayers = Array.isArray(firstData?.players)
        ? firstData.players
        : [];
      this.totalExpected = Number(
        firstData?.metadata?.total_players || firstPlayers.length,
      );
      const shardSize = Number(firstData?.metadata?.limit || limit);
      console.log(
        "[PlayerSearch] first shard size:",
        firstPlayers.length,
        "total expected:",
        this.totalExpected,
        "shard size:",
        shardSize,
      );

      // Initialize with first shard
      this.playerIndex = firstPlayers;
      this.isIndexLoaded = true;
      this.fuse = new Fuse(this.playerIndex, {
        keys: [
          { name: "name", weight: 0.6 },
          { name: "realm_name", weight: 0.2 },
          { name: "region", weight: 0.2 },
          { name: "class_name", weight: 0.1 },
          { name: "active_spec_name", weight: 0.1 },
        ],
        threshold: 0.35,
        includeScore: true,
      });

      // Continue fetching remaining shards in background
      const total = this.totalExpected;
      if (total > firstPlayers.length) {
        const totalShards = Math.ceil(total / shardSize);
        for (let s = 1; s < totalShards; s++) {
          const shardNum = s.toString().padStart(3, "0");
          const url = `/api/search/players-${shardNum}.json`;
          try {
            const res = await fetch(url);
            if (!res.ok) break;
            const data: PlayerSearchIndex = await res.json();
            const chunk = Array.isArray(data?.players) ? data.players : [];
            if (chunk.length === 0) break;
            // merge and rebuild index periodically
            this.playerIndex.push(...chunk);
            if (s % 2 === 0 || s === totalShards - 1) {
              this.fuse = new Fuse(this.playerIndex, {
                keys: [
                  { name: "name", weight: 0.6 },
                  { name: "realm_name", weight: 0.2 },
                  { name: "region", weight: 0.2 },
                  { name: "class_name", weight: 0.1 },
                  { name: "active_spec_name", weight: 0.1 },
                ],
                threshold: 0.35,
                includeScore: true,
              });
              // rerun current query if user typed something
              const q = this.searchInput.value.trim();
              if (q) this.performSearch(q);
            }
          } catch (e) {
            console.warn(`[PlayerSearch] shard ${shardNum} fetch failed:`, e);
            break;
          }
        }
        // Save final combined index
        this.saveToCache({
          players: this.playerIndex,
          metadata: { total_players: this.playerIndex.length },
        });
      } else {
        // Save small index
        this.saveToCache({
          players: this.playerIndex,
          metadata: firstData.metadata,
        });
      }
    } catch (e) {
      console.error("Failed to load player search index:", e);
      this.showNoResults();
    } finally {
      this.hideLoading();
    }
  }

  private setupIndex(data: PlayerSearchIndex) {
    this.playerIndex = data.players;
    this.isIndexLoaded = true;
    this.fuse = new Fuse(this.playerIndex, {
      keys: [
        { name: "name", weight: 0.6 },
        { name: "realm_name", weight: 0.2 },
        { name: "region", weight: 0.2 },
      ],
      threshold: 0.35,
      includeScore: true,
    });
  }

  private handleKeyboard(e: KeyboardEvent) {
    // If results are hidden, ignore navigation keys
    if (
      !this.resultsContainer.style.display ||
      this.resultsContainer.style.display === "none"
    )
      return;

    switch (e.key) {
      case "ArrowDown":
        e.preventDefault();
        this.selectedIndex = Math.min(
          this.selectedIndex + 1,
          this.filteredResults.length - 1,
        );
        this.updateSelection();
        break;
      case "ArrowUp":
        e.preventDefault();
        this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
        this.updateSelection();
        break;
      case "Tab": {
        // Cycle with Tab; Shift+Tab cycles backwards
        e.preventDefault();
        if (this.filteredResults.length === 0) return;
        if (e.shiftKey) {
          this.selectedIndex =
            this.selectedIndex <= 0
              ? this.filteredResults.length - 1
              : this.selectedIndex - 1;
        } else {
          this.selectedIndex =
            this.selectedIndex >= this.filteredResults.length - 1
              ? 0
              : this.selectedIndex + 1;
        }
        this.updateSelection();
        break;
      }
      case "Enter": {
        e.preventDefault();
        if (
          this.selectedIndex >= 0 &&
          this.selectedIndex < this.filteredResults.length
        ) {
          const sel = this.resultsList.querySelectorAll(".search-result-item")[
            this.selectedIndex
          ] as HTMLElement | null;
          sel?.click();
        } else {
          const first = this.resultsList.querySelector(
            ".search-result-item",
          ) as HTMLElement | null;
          first?.click();
        }
        break;
      }
      case "Escape":
        this.hideResults();
        break;
    }
  }

  private updateSelection() {
    const items = Array.from(
      this.resultsList.querySelectorAll(".search-result-item"),
    ) as HTMLElement[];
    items.forEach((el, idx) => {
      el.classList.toggle("selected", idx === this.selectedIndex);
      if (idx === this.selectedIndex) el.scrollIntoView({ block: "nearest" });
    });
  }

  private performSearch(query: string) {
    if (!this.fuse || !query) {
      this.resultsList.innerHTML = "";
      this.hideLoading();
      return;
    }
    this.filteredResults = this.fuse.search(query, { limit: 20 });
    if (!this.filteredResults.length) {
      this.showNoResults();
      return;
    }
    this.showResults();
    this.resultsList.innerHTML = "";
    this.filteredResults.forEach(({ item, score }, idx) => {
      const url = buildPlayerProfileURL(
        item.region || "us",
        item.realm_slug,
        item.name,
      );
      const el = document.createElement("div");
      el.className = "search-result-item";
      const relevance =
        score && score > 0.1
          ? `<span class="search-relevance" title="Relevance: ${(1 - score).toFixe
d(2)}">~</span>`
          : "";
      const color = item.class_name ? getClassColor(item.class_name) : "";
      el.innerHTML = `
        <div class="search-result-content">
          <div class="search-player-identity">
            ${relevance}<span class="search-player-name" style="color: ${color};">
${item.name}</span>
            <span class="search-region-badge">${(item.region || "").toUpperCase()}
</span>
            <span class="search-player-realm">${item.realm_name}</span>
          </div>
        </div>
      `;
      // mouse interaction highlights
      el.addEventListener("mouseenter", () => {
        this.selectedIndex = idx;
        this.updateSelection();
      });
      el.addEventListener("click", () => {
        window.location.href = url;
      });
      this.resultsList.appendChild(el);
    });
    // reset selection to first result
    this.selectedIndex = this.filteredResults.length > 0 ? 0 : -1;
    this.updateSelection();
  }
}

function initPlayerSearch() {
  const container = document.querySelector(
    ".player-search-island",
  ) as HTMLElement | null;
  if (!container) return;
  if (!(window as any).__playerSearchInit) {
    try {
      (window as any).__playerSearchInit = true;
      new PlayerSearchClient(container);
      console.log("[PlayerSearch] initialized");
    } catch (e) {
      console.error("[PlayerSearch] init failed:", e);
    }
  }
}

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", initPlayerSearch);
} else {
  initPlayerSearch();
}

import Fuse from "fuse.js";
import type { FuseResult } from "fuse.js";
import { buildPlayerProfileURL, debounce } f
rom "../../lib/utils.ts";
import { getClassColor } from "../../lib/cli
ent-utils.ts";

type PlayerSearchResult = {
  id: number;
  name: string;
  realm_slug: string;
  realm_name: string;
  region: string;
  class_name?: string;
  active_spec_name?: string;
  global_ranking?: number;
};
type PlayerSearchIndex = { players: PlayerSe
archResult[]; metadata: any };

class PlayerSearchClient {
  private container: HTMLElement;
  private searchInput: HTMLInputElement;
  private clearButton: HTMLButtonElement;
  private resultsContainer: HTMLElement;
  private resultsList: HTMLElement;
  private loadingDiv: HTMLElement;
  private noResultsDiv: HTMLElement;
  private playerIndex: PlayerSearchResult[] 
= [];
  private fuse: Fuse<PlayerSearchResult> | n
ull = null;
  private isIndexLoaded = false;
  private selectedIndex = -1;
  private filteredResults: FuseResult<Player
SearchResult>[] = [];
  private totalExpected = 0;

  constructor(container: HTMLElement) {
    this.container = container;
    this.searchInput = container.querySelect
or(
      "#player-search-input",
    ) as HTMLInputElement;
    this.clearButton = container.querySelect
or(
      "#search-clear",
    ) as HTMLButtonElement;
    this.resultsContainer = container.queryS
elector(
      "#search-results",
    ) as HTMLElement;
    this.resultsList = container.querySelect
or(
      "#search-results-list",
    ) as HTMLElement;
    this.loadingDiv = container.querySelecto
r("#search-loading") as HTMLElement;
    this.noResultsDiv = container.querySelec
tor(
      "#search-no-results",
    ) as HTMLElement;

    this.bindEvents();
    this.bindGlobalHotkeys();
  }

  private bindEvents() {
    const debouncedSearch = debounce((q: str
ing) => this.performSearch(q), 300);
    this.searchInput.addEventListener("input
", (e) => {
      if (!this.isIndexLoaded) this.loadPlay
erIndex(1000);
      const q = (e.target as HTMLInputElemen
t).value;
      this.updateClearButton(q);
      debouncedSearch(q);
    });
    this.searchInput.addEventListener("focus
", () => {
      if (!this.isIndexLoaded) this.loadPlay
erIndex(1000);
      if (this.searchInput.value.length > 0)
 this.showResults();
    });
    this.clearButton.addEventListener("click
", () => this.clearSearch());
    this.searchInput.addEventListener("keydo
wn", (e) => this.handleKeyboard(e));
    document.addEventListener("click", (e) =
> {
      if (!this.container.contains(e.target 
as Node)) this.hideResults();
    });
  }

  private bindGlobalHotkeys() {
    document.addEventListener("keydown", (e)
 => {
      if (e.key === "/" && !e.ctrlKey && !e.
metaKey && !e.altKey) {
        const tag =
          (document.activeElement &&
            (document.activeElement as HTMLE
lement).tagName) ||
          "";
        if (tag !== "INPUT" && tag !== "TEXT
AREA") {
          e.preventDefault();
          this.searchInput.focus();
          this.searchInput.value = "";
          this.showResults();
        }
      }
    });
  }

  private updateClearButton(q: string) {
    this.clearButton.style.display = q.lengt
h > 0 ? "block" : "none";
  }

  private clearSearch() {
    this.searchInput.value = "";
    this.clearButton.style.display = "none";
    this.hideResults();
    this.searchInput.focus();
  }

  private showResults() {
    this.resultsContainer.style.display = "b
lock";
    this.noResultsDiv.style.display = "none"
;
    this.resultsList.style.display = "block"
;
  }
  private hideResults() {
    this.resultsContainer.style.display = "n
one";
    this.selectedIndex = -1;
  }
  private showLoading() {
    this.resultsContainer.style.display = "b
lock";
    this.loadingDiv.style.display = "flex";
    this.resultsList.style.display = "none";
    this.noResultsDiv.style.display = "none"
;
  }
  private hideLoading() {
    this.loadingDiv.style.display = "none";
  }
  private showNoResults() {
    this.resultsContainer.style.display = "b
lock";
    this.noResultsDiv.style.display = "block
";
    this.resultsList.style.display = "none";
    this.loadingDiv.style.display = "none";
  }

  private loadFromCache(): PlayerSearchIndex
 | null {
    try {
      const cached = localStorage.getItem("p
layerSearchIndex");
      if (!cached) return null;
      const data = JSON.parse(cached);
      if (!data?.players || data.players.len
gth === 0) {
        console.log("[PlayerSearch] cache pr
esent but empty; ignoring");
        localStorage.removeItem("playerSearc
hIndex");
        return null;
      }
      const age = Date.now() - new Date(data
.metadata.cached_at).getTime();
      if (age < 24 * 60 * 60 * 1000) return 
data;
      localStorage.removeItem("playerSearchI
ndex");
      return null;
    } catch {
      localStorage.removeItem("playerSearchI
ndex");
      return null;
    }
  }

  private saveToCache(data: PlayerSearchInde
x) {
    try {
      const payload = {
        ...data,
        metadata: { ...data.metadata, cached
_at: new Date().toISOString() },
      };
      localStorage.setItem("playerSearchInde
x", JSON.stringify(payload));
    } catch {}
  }

  private async loadPlayerIndex(limit = 5000
) {
    if (this.isIndexLoaded) return;
    const cached = this.loadFromCache();
    if (cached) {
      console.log("[PlayerSearch] using cach
ed index:", cached.players.length);
      this.setupIndex(cached);
      return;
    }
    this.showLoading();
    try {
      // Fetch first shard to get total and 
initialize
      const firstUrl = `/api/search/players-
000.json`;
      console.log("[PlayerSearch] fetching i
ndex:", firstUrl);
      const firstRes = await fetch(firstUrl)
;
      console.log(
        "[PlayerSearch] index response:",
        firstRes.status,
        firstRes.statusText,
      );
      if (!firstRes.ok)
        throw new Error(`${firstRes.status} 
${firstRes.statusText}`);
      const firstData: PlayerSearchIndex = a
wait firstRes.json();
      const firstPlayers = Array.isArray(fir
stData?.players)
        ? firstData.players
        : [];
      this.totalExpected = Number(
        firstData?.metadata?.total_players |
| firstPlayers.length,
      );
      const shardSize = Number(firstData?.me
tadata?.limit || limit);
      console.log(
        "[PlayerSearch] first shard size:",
        firstPlayers.length,
        "total expected:",
        this.totalExpected,
        "shard size:",
        shardSize,
      );

      // Initialize with first shard
      this.playerIndex = firstPlayers;
      this.isIndexLoaded = true;
      this.fuse = new Fuse(this.playerIndex,
 {
        keys: [
          { name: "name", weight: 0.6 },
          { name: "realm_name", weight: 0.2 
},
          { name: "region", weight: 0.2 },
          { name: "class_name", weight: 0.1 
},
          { name: "active_spec_name", weight
: 0.1 },
        ],
        threshold: 0.35,
        includeScore: true,
      });

      // Continue fetching remaining shards 
in background
      const total = this.totalExpected;
      if (total > firstPlayers.length) {
        const totalShards = Math.ceil(total 
/ shardSize);
        for (let s = 1; s < totalShards; s++
) {
          const shardNum = s.toString().padS
tart(3, "0");
          const url = `/api/search/players-$
{shardNum}.json`;
          try {
            const res = await fetch(url);
            if (!res.ok) break;
            const data: PlayerSearchIndex = 
await res.json();
            const chunk = Array.isArray(data
?.players) ? data.players : [];
            if (chunk.length === 0) break;
            // merge and rebuild index perio
dically
            this.playerIndex.push(...chunk);
            if (s % 2 === 0 || s === totalSh
ards - 1) {
              this.fuse = new Fuse(this.play
erIndex, {
                keys: [
                  { name: "name", weight: 0.
6 },
                  { name: "realm_name", weig
ht: 0.2 },
                  { name: "region", weight: 
0.2 },
                  { name: "class_name", weig
ht: 0.1 },
                  { name: "active_spec_name"
, weight: 0.1 },
                ],
                threshold: 0.35,
                includeScore: true,
              });
              // rerun current query if user
 typed something
              const q = this.searchInput.val
ue.trim();
              if (q) this.performSearch(q);
            }
          } catch (e) {
            console.warn(`[PlayerSearch] sha
rd ${shardNum} fetch failed:`, e);
            break;
          }
        }
        // Save final combined index
        this.saveToCache({
          players: this.playerIndex,
          metadata: { total_players: this.pl
ayerIndex.length },
        });
      } else {
        // Save small index
        this.saveToCache({
          players: this.playerIndex,
          metadata: firstData.metadata,
        });
      }
    } catch (e) {
      console.error("Failed to load player s
earch index:", e);
      this.showNoResults();
    } finally {
      this.hideLoading();
    }
  }

  private setupIndex(data: PlayerSearchIndex
) {
    this.playerIndex = data.players;
    this.isIndexLoaded = true;
    this.fuse = new Fuse(this.playerIndex, {
      keys: [
        { name: "name", weight: 0.6 },
        { name: "realm_name", weight: 0.2 },
        { name: "region", weight: 0.2 },
      ],
      threshold: 0.35,
      includeScore: true,
    });
  }

  private handleKeyboard(e: KeyboardEvent) {
    // If results are hidden, ignore navigat
ion keys
    if (
      !this.resultsContainer.style.display |
|
      this.resultsContainer.style.display ==
= "none"
    )
      return;

    switch (e.key) {
      case "ArrowDown":
        e.preventDefault();
        this.selectedIndex = Math.min(
          this.selectedIndex + 1,
          this.filteredResults.length - 1,
        );
        this.updateSelection();
        break;
      case "ArrowUp":
        e.preventDefault();
        this.selectedIndex = Math.max(this.s
electedIndex - 1, 0);
        this.updateSelection();
        break;
      case "Tab": {
        // Cycle with Tab; Shift+Tab cycles 
backwards
        e.preventDefault();
        if (this.filteredResults.length === 
0) return;
        if (e.shiftKey) {
          this.selectedIndex =
            this.selectedIndex <= 0
              ? this.filteredResults.length 
- 1
              : this.selectedIndex - 1;
        } else {
          this.selectedIndex =
            this.selectedIndex >= this.filte
redResults.length - 1
              ? 0
              : this.selectedIndex + 1;
        }
        this.updateSelection();
        break;
      }
      case "Enter": {
        e.preventDefault();
        if (
          this.selectedIndex >= 0 &&
          this.selectedIndex < this.filtered
Results.length
        ) {
          const sel = this.resultsList.query
SelectorAll(".search-result-item")[
            this.selectedIndex
          ] as HTMLElement | null;
          sel?.click();
        } else {
          const first = this.resultsList.que
rySelector(
            ".search-result-item",
          ) as HTMLElement | null;
          first?.click();
        }
        break;
      }
      case "Escape":
        this.hideResults();
        break;
    }
  }

  private updateSelection() {
    const items = Array.from(
      this.resultsList.querySelectorAll(".se
arch-result-item"),
    ) as HTMLElement[];
    items.forEach((el, idx) => {
      el.classList.toggle("selected", idx ==
= this.selectedIndex);
      if (idx === this.selectedIndex) el.scr
ollIntoView({ block: "nearest" });
    });
  }

  private performSearch(query: string) {
    if (!this.fuse || !query) {
      this.resultsList.innerHTML = "";
      this.hideLoading();
      return;
    }
    this.filteredResults = this.fuse.search(
query, { limit: 20 });
    if (!this.filteredResults.length) {
      this.showNoResults();
      return;
    }
    this.showResults();
    this.resultsList.innerHTML = "";
    this.filteredResults.forEach(({ item, sc
ore }, idx) => {
      const url = buildPlayerProfileURL(
        item.region || "us",
        item.realm_slug,
        item.name,
      );
      const el = document.createElement("div
");
      el.className = "search-result-item";
      const relevance =
        score && score > 0.1
          ? `<span class="search-relevance" 
title="Relevance: ${(1 - score).toFixed(2)}"
>~</span>`
          : "";
      const color = item.class_name ? getCla
ssColor(item.class_name) : "";
      el.innerHTML = `
        <div class="search-result-content">
          <div class="search-player-identity
">
            ${relevance}<span class="search-
player-name" style="color: ${color};">${item
.name}</span>
            <span class="search-region-badge
">${(item.region || "").toUpperCase()}</span
>
            <span class="search-player-realm
">${item.realm_name}</span>
          </div>
        </div>
      `;
      // mouse interaction highlights
      el.addEventListener("mouseenter", () =
> {
        this.selectedIndex = idx;
        this.updateSelection();
      });
      el.addEventListener("click", () => {
        window.location.href = url;
      });
      this.resultsList.appendChild(el);
    });
    // reset selection to first result
    this.selectedIndex = this.filteredResult
s.length > 0 ? 0 : -1;
    this.updateSelection();
  }
}

function initPlayerSearch() {
  const container = document.querySelector(
    ".player-search-island",
  ) as HTMLElement | null;
  if (!container) return;
  if (!(window as any).__playerSearchInit) {
    try {
      (window as any).__playerSearchInit = t
rue;
      new PlayerSearchClient(container);
      console.log("[PlayerSearch] initialize
d");
    } catch (e) {
      console.error("[PlayerSearch] init fai
led:", e);
    }
  }
}

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoade
d", initPlayerSearch);
} else {
  initPlayerSearch();
}
 
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET