HASH d76ccaeefab2
DATE 2026-05-02
SUBJECT web: add ItemTooltip component
FILES 5 CHANGED
HASH d76ccaeefab2
DATE 2026-05-02
SUBJECT web: add ItemTooltip component
FILES 5 CHANGED
┌─ web/src/components/ItemTooltip/ItemTooltip.astro ─────────────────────────┐
│ diff --git a/web/src/components/ItemTooltip/ItemTooltip.astro b/web/src/components │
│ /ItemTooltip/ItemTooltip.astro │
│ new file mode 100644 │
│ index 0000000..31867e6 │
│ --- /dev/null │
│ +++ b/web/src/components/ItemTooltip/ItemTooltip.astro │
│ @@ -0,0 +1,24 @@ │
│ +--- │
│ +import "./ItemTooltip.scss"; │
│ +--- │
│ + │
│ +<div id="item-tooltip" class="item-tooltip" style="display:none"> │
│ + <div class="tooltip-header"> │
│ + <span class="tooltip-item-name"></span> │
│ + </div> │
│ + <div class="tooltip-ilvl"></div> │
│ + <div class="tooltip-slot-armor"></div> │
│ + <div class="tooltip-stats"></div> │
│ + <div class="tooltip-gems"></div> │
│ + <div class="tooltip-enchant"></div> │
│ + <div class="tooltip-effect"></div> │
│ + <div class="tooltip-set"></div> │
│ +</div> │
│ + │
│ +<script> │
│ + import { initItemTooltip } from "./ItemTooltip.ts"; │
│ + // run on initial load │
│ + initItemTooltip(); │
│ + // also handle View Transitions if enabled │
│ + document.addEventListener("astro:page-load", initItemTooltip); │
│ +</script> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...ItemTooltip/ItemTooltip.astro ───┐
│ diff --git a/web/src/components/ItemTooltip/ │
│ ItemTooltip.astro b/web/src/components/ItemT │
│ ooltip/ItemTooltip.astro │
│ new file mode 100644 │
│ index 0000000..31867e6 │
│ --- /dev/null │
│ +++ b/web/src/components/ItemTooltip/ItemToo │
│ ltip.astro │
│ @@ -0,0 +1,24 @@ │
│ +--- │
│ +import "./ItemTooltip.scss"; │
│ +--- │
│ + │
│ +<div id="item-tooltip" class="item-tooltip" │
│ style="display:none"> │
│ + <div class="tooltip-header"> │
│ + <span class="tooltip-item-name"></span> │
│ + </div> │
│ + <div class="tooltip-ilvl"></div> │
│ + <div class="tooltip-slot-armor"></div> │
│ + <div class="tooltip-stats"></div> │
│ + <div class="tooltip-gems"></div> │
│ + <div class="tooltip-enchant"></div> │
│ + <div class="tooltip-effect"></div> │
│ + <div class="tooltip-set"></div> │
│ +</div> │
│ + │
│ +<script> │
│ + import { initItemTooltip } from "./ItemTo │
│ oltip.ts"; │
│ + // run on initial load │
│ + initItemTooltip(); │
│ + // also handle View Transitions if enable │
│ d │
│ + document.addEventListener("astro:page-loa │
│ d", initItemTooltip); │
│ +</script> │
└──────────────────────────────────────────────┘
┌─ web/src/components/ItemTooltip/ItemTooltip.scss ──────────────────────────┐
│ diff --git a/web/src/components/ItemTooltip/ItemTooltip.scss b/web/src/components/ │
│ ItemTooltip/ItemTooltip.scss │
│ new file mode 100644 │
│ index 0000000..c362719 │
│ --- /dev/null │
│ +++ b/web/src/components/ItemTooltip/ItemTooltip.scss │
│ @@ -0,0 +1,94 @@ │
│ +.item-tooltip { │
│ + position: fixed; │
│ + z-index: 9999; │
│ + pointer-events: none; │
│ + min-width: 200px; │
│ + max-width: 320px; │
│ + padding: 10px 14px; │
│ + background: #1a1a1a; │
│ + border: 1px solid #4d4947; │
│ + border-radius: 4px; │
│ + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6); │
│ + font-size: 13px; │
│ + line-height: 1.5; │
│ + color: #e6e6e6; │
│ +} │
│ + │
│ +.tooltip-item-name { │
│ + display: block; │
│ + font-size: 15px; │
│ + font-weight: 600; │
│ + margin-bottom: 2px; │
│ + │
│ + &.quality-poor { color: var(--quality-poor); } │
│ + &.quality-common { color: var(--quality-common); } │
│ + &.quality-uncommon { color: var(--quality-uncommon); } │
│ + &.quality-rare { color: var(--quality-rare); } │
│ + &.quality-epic { color: var(--quality-epic); } │
│ + &.quality-legendary { color: var(--quality-legendary); } │
│ + &.quality-artifact { color: var(--quality-artifact); } │
│ + &.quality-heirloom { color: var(--quality-heirloom); } │
│ +} │
│ + │
│ +.tooltip-ilvl { │
│ + color: var(--quality-heirloom); │
│ + margin-bottom: 4px; │
│ +} │
│ + │
│ +.tooltip-slot-armor { │
│ + color: #e6e6e6; │
│ + margin-bottom: 4px; │
│ +} │
│ + │
│ +.tooltip-stats { │
│ + margin-bottom: 4px; │
│ +} │
│ + │
│ +.tooltip-stat-line { │
│ + color: #fff; │
│ +} │
│ + │
│ +.tooltip-gems { │
│ + margin-bottom: 4px; │
│ +} │
│ + │
│ +.tooltip-gem-line { │
│ + display: flex; │
│ + align-items: center; │
│ + gap: 4px; │
│ + color: #fff; │
│ +} │
│ + │
│ +.tooltip-gem-icon { │
│ + width: 14px; │
│ + height: 14px; │
│ + border-radius: 2px; │
│ + flex-shrink: 0; │
│ +} │
│ + │
│ +.tooltip-enchant { │
│ + margin-bottom: 4px; │
│ +} │
│ + │
│ +.tooltip-enchant-line { │
│ + color: var(--quality-uncommon); │
│ +} │
│ + │
│ +.tooltip-effect { │
│ + margin-top: 6px; │
│ + padding-top: 6px; │
│ + border-top: 1px solid #333; │
│ +} │
│ + │
│ +.tooltip-effect:empty { │
│ + display: none; │
│ +} │
│ + │
│ +.tooltip-effect-line { │
│ + color: #00ff00; // Blizzard tooltip green │
│ +} │
│ + │
│ +.tooltip-set { │
│ + color: var(--quality-uncommon); │
│ + margin-top: 4px; │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ .../ItemTooltip/ItemTooltip.scss ───┐
│ diff --git a/web/src/components/ItemTooltip/ │
│ ItemTooltip.scss b/web/src/components/ItemTo │
│ oltip/ItemTooltip.scss │
│ new file mode 100644 │
│ index 0000000..c362719 │
│ --- /dev/null │
│ +++ b/web/src/components/ItemTooltip/ItemToo │
│ ltip.scss │
│ @@ -0,0 +1,94 @@ │
│ +.item-tooltip { │
│ + position: fixed; │
│ + z-index: 9999; │
│ + pointer-events: none; │
│ + min-width: 200px; │
│ + max-width: 320px; │
│ + padding: 10px 14px; │
│ + background: #1a1a1a; │
│ + border: 1px solid #4d4947; │
│ + border-radius: 4px; │
│ + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6) │
│ ; │
│ + font-size: 13px; │
│ + line-height: 1.5; │
│ + color: #e6e6e6; │
│ +} │
│ + │
│ +.tooltip-item-name { │
│ + display: block; │
│ + font-size: 15px; │
│ + font-weight: 600; │
│ + margin-bottom: 2px; │
│ + │
│ + &.quality-poor { color: var(--quality-poo │
│ r); } │
│ + &.quality-common { color: var(--quality-c │
│ ommon); } │
│ + &.quality-uncommon { color: var(--quality │
│ -uncommon); } │
│ + &.quality-rare { color: var(--quality-rar │
│ e); } │
│ + &.quality-epic { color: var(--quality-epi │
│ c); } │
│ + &.quality-legendary { color: var(--qualit │
│ y-legendary); } │
│ + &.quality-artifact { color: var(--quality │
│ -artifact); } │
│ + &.quality-heirloom { color: var(--quality │
│ -heirloom); } │
│ +} │
│ + │
│ +.tooltip-ilvl { │
│ + color: var(--quality-heirloom); │
│ + margin-bottom: 4px; │
│ +} │
│ + │
│ +.tooltip-slot-armor { │
│ + color: #e6e6e6; │
│ + margin-bottom: 4px; │
│ +} │
│ + │
│ +.tooltip-stats { │
│ + margin-bottom: 4px; │
│ +} │
│ + │
│ +.tooltip-stat-line { │
│ + color: #fff; │
│ +} │
│ + │
│ +.tooltip-gems { │
│ + margin-bottom: 4px; │
│ +} │
│ + │
│ +.tooltip-gem-line { │
│ + display: flex; │
│ + align-items: center; │
│ + gap: 4px; │
│ + color: #fff; │
│ +} │
│ + │
│ +.tooltip-gem-icon { │
│ + width: 14px; │
│ + height: 14px; │
│ + border-radius: 2px; │
│ + flex-shrink: 0; │
│ +} │
│ + │
│ +.tooltip-enchant { │
│ + margin-bottom: 4px; │
│ +} │
│ + │
│ +.tooltip-enchant-line { │
│ + color: var(--quality-uncommon); │
│ +} │
│ + │
│ +.tooltip-effect { │
│ + margin-top: 6px; │
│ + padding-top: 6px; │
│ + border-top: 1px solid #333; │
│ +} │
│ + │
│ +.tooltip-effect:empty { │
│ + display: none; │
│ +} │
│ + │
│ +.tooltip-effect-line { │
│ + color: #00ff00; // Blizzard tooltip green │
│ +} │
│ + │
│ +.tooltip-set { │
│ + color: var(--quality-uncommon); │
│ + margin-top: 4px; │
│ +} │
└──────────────────────────────────────────────┘
┌─ web/src/components/ItemTooltip/ItemTooltip.ts ────────────────────────────┐
│ diff --git a/web/src/components/ItemTooltip/ItemTooltip.ts b/web/src/components/It │
│ emTooltip/ItemTooltip.ts │
│ new file mode 100644 │
│ index 0000000..111646a │
│ --- /dev/null │
│ +++ b/web/src/components/ItemTooltip/ItemTooltip.ts │
│ @@ -0,0 +1,183 @@ │
│ +import type { Enchantment, ScalingOption } from "../../lib/types"; │
│ + │
│ +const STAT_NAMES: Record<string, string> = { │
│ + "0": "Strength", │
│ + "1": "Agility", │
│ + "2": "Stamina", │
│ + "3": "Intellect", │
│ + "4": "Spirit", │
│ + "5": "Hit", │
│ + "6": "Crit", │
│ + "7": "Haste", │
│ + "8": "Expertise", │
│ + "9": "Dodge", │
│ + "10": "Parry", │
│ + "11": "Mastery", │
│ +}; │
│ + │
│ +function getScaling(scalingOptions: Record<string, ScalingOption>): ScalingOption │
│ | null { │
│ + return scalingOptions["0"] ?? null; │
│ +} │
│ + │
│ +function populateTooltip( │
│ + tooltip: HTMLElement, │
│ + scalingOptions: Record<string, ScalingOption>, │
│ + itemName: string, │
│ + quality: string, │
│ + enchants: Enchantment[], │
│ + spellDescription: string | null, │
│ +) { │
│ + const nameEl = tooltip.querySelector<HTMLElement>(".tooltip-item-name"); │
│ + const ilvlEl = tooltip.querySelector<HTMLElement>(".tooltip-ilvl"); │
│ + const slotArmorEl = tooltip.querySelector<HTMLElement>(".tooltip-slot-armor"); │
│ + const statsEl = tooltip.querySelector<HTMLElement>(".tooltip-stats"); │
│ + const gemsEl = tooltip.querySelector<HTMLElement>(".tooltip-gems"); │
│ + const enchantEl = tooltip.querySelector<HTMLElement>(".tooltip-enchant"); │
│ + const effectEl = tooltip.querySelector<HTMLElement>(".tooltip-effect"); │
│ + const setEl = tooltip.querySelector<HTMLElement>(".tooltip-set"); │
│ + │
│ + if (nameEl) { │
│ + nameEl.textContent = itemName; │
│ + nameEl.className = `tooltip-item-name quality-${quality.toLowerCase()}`; │
│ + } │
│ + │
│ + const scaling = getScaling(scalingOptions); │
│ + │
│ + if (ilvlEl) { │
│ + ilvlEl.textContent = scaling?.ilvl ? `Item Level ${scaling.ilvl}` : ""; │
│ + } │
│ + │
│ + if (slotArmorEl) { │
│ + const parts: string[] = []; │
│ + if (scaling?.stats?.["17"]) { │
│ + parts.push(`${scaling.stats["17"].toLocaleString()} Armor`); │
│ + } │
│ + slotArmorEl.textContent = parts.join(" - "); │
│ + } │
│ + │
│ + // stats (skip armor key 17) │
│ + if (statsEl) { │
│ + statsEl.innerHTML = ""; │
│ + if (scaling?.stats) { │
│ + for (const [key, value] of Object.entries(scaling.stats)) { │
│ + if (key === "17") continue; │
│ + const name = STAT_NAMES[key] || `Stat ${key}`; │
│ + const div = document.createElement("div"); │
│ + div.className = "tooltip-stat-line"; │
│ + div.textContent = `+${value.toLocaleString()} ${name}`; │
│ + statsEl.appendChild(div); │
│ + } │
│ + } │
│ + } │
│ + │
│ + // gems (slot_id 2-5, no slot_type) │
│ + if (gemsEl) { │
│ + gemsEl.innerHTML = ""; │
│ + const gems = enchants.filter( │
│ + (e) => e.slot_id >= 2 && e.slot_id <= 5 && !e.slot_type, │
│ + ); │
│ + for (const gem of gems) { │
│ + const div = document.createElement("div"); │
│ + div.className = "tooltip-gem-line"; │
│ + if (gem.gem_icon_slug) { │
│ + const img = document.createElement("img"); │
│ + img.src = `https://wow.zamimg.com/images/wow/icons/small/${gem.gem_icon_s │
│ lug}.jpg`; │
│ + img.className = "tooltip-gem-icon"; │
│ + img.alt = ""; │
│ + div.appendChild(img); │
│ + } │
│ + const span = document.createElement("span"); │
│ + span.textContent = gem.display_string; │
│ + div.appendChild(span); │
│ + gemsEl.appendChild(div); │
│ + } │
│ + } │
│ + │
│ + // enchant (slot_type === "PERMANENT") │
│ + if (enchantEl) { │
│ + enchantEl.innerHTML = ""; │
│ + const permanent = enchants.filter((e) => e.slot_type === "PERMANENT"); │
│ + for (const ench of permanent) { │
│ + const div = document.createElement("div"); │
│ + div.className = "tooltip-enchant-line"; │
│ + div.textContent = ench.display_string; │
│ + enchantEl.appendChild(div); │
│ + } │
│ + } │
│ + │
│ + // spell description from Blizzard API │
│ + if (effectEl) { │
│ + effectEl.innerHTML = ""; │
│ + if (spellDescription) { │
│ + // handle multiple descriptions (separated by newlines) │
│ + const descriptions = spellDescription.split("\n"); │
│ + for (const desc of descriptions) { │
│ + if (desc.trim()) { │
│ + const div = document.createElement("div"); │
│ + div.className = "tooltip-effect-line"; │
│ + div.textContent = desc; │
│ + effectEl.appendChild(div); │
│ + } │
│ + } │
│ + } │
│ + } │
│ + │
│ + if (setEl) { │
│ + setEl.textContent = ""; │
│ + setEl.style.display = "none"; │
│ + } │
│ +} │
│ + │
│ +function positionTooltip(tooltip: HTMLElement, anchor: HTMLElement) { │
│ + const rect = anchor.getBoundingClientRect(); │
│ + const tooltipRect = tooltip.getBoundingClientRect(); │
│ + const padding = 8; │
│ + │
│ + let left = rect.right + padding; │
│ + let top = rect.top; │
│ + │
│ + if (left + tooltipRect.width > window.innerWidth) { │
│ + left = rect.left - tooltipRect.width - padding; │
│ + } │
│ + if (top + tooltipRect.height > window.innerHeight) { │
│ + top = window.innerHeight - tooltipRect.height - padding; │
│ + } │
│ + if (top < padding) top = padding; │
│ + │
│ + tooltip.style.left = `${left}px`; │
│ + tooltip.style.top = `${top}px`; │
│ +} │
│ + │
│ +export function initItemTooltip() { │
│ + const tooltip = document.getElementById("item-tooltip"); │
│ + if (!tooltip) return; │
│ + │
│ + const icons = document.querySelectorAll<HTMLElement>(".equip-icon-wrap[data-ite │
│ m-id]"); │
│ + │
│ + for (const icon of icons) { │
│ + icon.addEventListener("mouseenter", () => { │
│ + const itemName = icon.dataset.itemName || "Unknown"; │
│ + const quality = icon.dataset.quality || "COMMON"; │
│ + │
│ + let enchants: Enchantment[] = []; │
│ + try { │
│ + enchants = JSON.parse(icon.dataset.enchants || "[]"); │
│ + } catch { /* ignore */ } │
│ + │
│ + let scalingOptions: Record<string, ScalingOption> = {}; │
│ + try { │
│ + scalingOptions = JSON.parse(icon.dataset.scaling || "{}"); │
│ + } catch { /* ignore */ } │
│ + │
│ + const spellDescription = icon.dataset.spellDescription || null; │
│ + │
│ + populateTooltip(tooltip, scalingOptions, itemName, quality, enchants, spell │
│ Description); │
│ + tooltip.style.display = "block"; │
│ + positionTooltip(tooltip, icon); │
│ + }); │
│ + │
│ + icon.addEventListener("mouseleave", () => { │
│ + tooltip.style.display = "none"; │
│ + }); │
│ + } │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...ts/ItemTooltip/ItemTooltip.ts ───┐
│ diff --git a/web/src/components/ItemTooltip/ │
│ ItemTooltip.ts b/web/src/components/ItemTool │
│ tip/ItemTooltip.ts │
│ new file mode 100644 │
│ index 0000000..111646a │
│ --- /dev/null │
│ +++ b/web/src/components/ItemTooltip/ItemToo │
│ ltip.ts │
│ @@ -0,0 +1,183 @@ │
│ +import type { Enchantment, ScalingOption } │
│ from "../../lib/types"; │
│ + │
│ +const STAT_NAMES: Record<string, string> = │
│ { │
│ + "0": "Strength", │
│ + "1": "Agility", │
│ + "2": "Stamina", │
│ + "3": "Intellect", │
│ + "4": "Spirit", │
│ + "5": "Hit", │
│ + "6": "Crit", │
│ + "7": "Haste", │
│ + "8": "Expertise", │
│ + "9": "Dodge", │
│ + "10": "Parry", │
│ + "11": "Mastery", │
│ +}; │
│ + │
│ +function getScaling(scalingOptions: Record< │
│ string, ScalingOption>): ScalingOption | nul │
│ l { │
│ + return scalingOptions["0"] ?? null; │
│ +} │
│ + │
│ +function populateTooltip( │
│ + tooltip: HTMLElement, │
│ + scalingOptions: Record<string, ScalingOpt │
│ ion>, │
│ + itemName: string, │
│ + quality: string, │
│ + enchants: Enchantment[], │
│ + spellDescription: string | null, │
│ +) { │
│ + const nameEl = tooltip.querySelector<HTML │
│ Element>(".tooltip-item-name"); │
│ + const ilvlEl = tooltip.querySelector<HTML │
│ Element>(".tooltip-ilvl"); │
│ + const slotArmorEl = tooltip.querySelector │
│ <HTMLElement>(".tooltip-slot-armor"); │
│ + const statsEl = tooltip.querySelector<HTM │
│ LElement>(".tooltip-stats"); │
│ + const gemsEl = tooltip.querySelector<HTML │
│ Element>(".tooltip-gems"); │
│ + const enchantEl = tooltip.querySelector<H │
│ TMLElement>(".tooltip-enchant"); │
│ + const effectEl = tooltip.querySelector<HT │
│ MLElement>(".tooltip-effect"); │
│ + const setEl = tooltip.querySelector<HTMLE │
│ lement>(".tooltip-set"); │
│ + │
│ + if (nameEl) { │
│ + nameEl.textContent = itemName; │
│ + nameEl.className = `tooltip-item-name q │
│ uality-${quality.toLowerCase()}`; │
│ + } │
│ + │
│ + const scaling = getScaling(scalingOptions │
│ ); │
│ + │
│ + if (ilvlEl) { │
│ + ilvlEl.textContent = scaling?.ilvl ? `I │
│ tem Level ${scaling.ilvl}` : ""; │
│ + } │
│ + │
│ + if (slotArmorEl) { │
│ + const parts: string[] = []; │
│ + if (scaling?.stats?.["17"]) { │
│ + parts.push(`${scaling.stats["17"].toL │
│ ocaleString()} Armor`); │
│ + } │
│ + slotArmorEl.textContent = parts.join(" │
│ - "); │
│ + } │
│ + │
│ + // stats (skip armor key 17) │
│ + if (statsEl) { │
│ + statsEl.innerHTML = ""; │
│ + if (scaling?.stats) { │
│ + for (const [key, value] of Object.ent │
│ ries(scaling.stats)) { │
│ + if (key === "17") continue; │
│ + const name = STAT_NAMES[key] || `St │
│ at ${key}`; │
│ + const div = document.createElement( │
│ "div"); │
│ + div.className = "tooltip-stat-line" │
│ ; │
│ + div.textContent = `+${value.toLocal │
│ eString()} ${name}`; │
│ + statsEl.appendChild(div); │
│ + } │
│ + } │
│ + } │
│ + │
│ + // gems (slot_id 2-5, no slot_type) │
│ + if (gemsEl) { │
│ + gemsEl.innerHTML = ""; │
│ + const gems = enchants.filter( │
│ + (e) => e.slot_id >= 2 && e.slot_id <= │
│ 5 && !e.slot_type, │
│ + ); │
│ + for (const gem of gems) { │
│ + const div = document.createElement("d │
│ iv"); │
│ + div.className = "tooltip-gem-line"; │
│ + if (gem.gem_icon_slug) { │
│ + const img = document.createElement( │
│ "img"); │
│ + img.src = `https://wow.zamimg.com/i │
│ mages/wow/icons/small/${gem.gem_icon_slug}.j │
│ pg`; │
│ + img.className = "tooltip-gem-icon"; │
│ + img.alt = ""; │
│ + div.appendChild(img); │
│ + } │
│ + const span = document.createElement(" │
│ span"); │
│ + span.textContent = gem.display_string │
│ ; │
│ + div.appendChild(span); │
│ + gemsEl.appendChild(div); │
│ + } │
│ + } │
│ + │
│ + // enchant (slot_type === "PERMANENT") │
│ + if (enchantEl) { │
│ + enchantEl.innerHTML = ""; │
│ + const permanent = enchants.filter((e) = │
│ > e.slot_type === "PERMANENT"); │
│ + for (const ench of permanent) { │
│ + const div = document.createElement("d │
│ iv"); │
│ + div.className = "tooltip-enchant-line │
│ "; │
│ + div.textContent = ench.display_string │
│ ; │
│ + enchantEl.appendChild(div); │
│ + } │
│ + } │
│ + │
│ + // spell description from Blizzard API │
│ + if (effectEl) { │
│ + effectEl.innerHTML = ""; │
│ + if (spellDescription) { │
│ + // handle multiple descriptions (sepa │
│ rated by newlines) │
│ + const descriptions = spellDescription │
│ .split("\n"); │
│ + for (const desc of descriptions) { │
│ + if (desc.trim()) { │
│ + const div = document.createElemen │
│ t("div"); │
│ + div.className = "tooltip-effect-l │
│ ine"; │
│ + div.textContent = desc; │
│ + effectEl.appendChild(div); │
│ + } │
│ + } │
│ + } │
│ + } │
│ + │
│ + if (setEl) { │
│ + setEl.textContent = ""; │
│ + setEl.style.display = "none"; │
│ + } │
│ +} │
│ + │
│ +function positionTooltip(tooltip: HTMLEleme │
│ nt, anchor: HTMLElement) { │
│ + const rect = anchor.getBoundingClientRect │
│ (); │
│ + const tooltipRect = tooltip.getBoundingCl │
│ ientRect(); │
│ + const padding = 8; │
│ + │
│ + let left = rect.right + padding; │
│ + let top = rect.top; │
│ + │
│ + if (left + tooltipRect.width > window.inn │
│ erWidth) { │
│ + left = rect.left - tooltipRect.width - │
│ padding; │
│ + } │
│ + if (top + tooltipRect.height > window.inn │
│ erHeight) { │
│ + top = window.innerHeight - tooltipRect. │
│ height - padding; │
│ + } │
│ + if (top < padding) top = padding; │
│ + │
│ + tooltip.style.left = `${left}px`; │
│ + tooltip.style.top = `${top}px`; │
│ +} │
│ + │
│ +export function initItemTooltip() { │
│ + const tooltip = document.getElementById(" │
│ item-tooltip"); │
│ + if (!tooltip) return; │
│ + │
│ + const icons = document.querySelectorAll<H │
│ TMLElement>(".equip-icon-wrap[data-item-id]" │
│ ); │
│ + │
│ + for (const icon of icons) { │
│ + icon.addEventListener("mouseenter", () │
│ => { │
│ + const itemName = icon.dataset.itemNam │
│ e || "Unknown"; │
│ + const quality = icon.dataset.quality │
│ || "COMMON"; │
│ + │
│ + let enchants: Enchantment[] = []; │
│ + try { │
│ + enchants = JSON.parse(icon.dataset. │
│ enchants || "[]"); │
│ + } catch { /* ignore */ } │
│ + │
│ + let scalingOptions: Record<string, Sc │
│ alingOption> = {}; │
│ + try { │
│ + scalingOptions = JSON.parse(icon.da │
│ taset.scaling || "{}"); │
│ + } catch { /* ignore */ } │
│ + │
│ + const spellDescription = icon.dataset │
│ .spellDescription || null; │
│ + │
│ + populateTooltip(tooltip, scalingOptio │
│ ns, itemName, quality, enchants, spellDescri │
│ ption); │
│ + tooltip.style.display = "block"; │
│ + positionTooltip(tooltip, icon); │
│ + }); │
│ + │
│ + icon.addEventListener("mouseleave", () │
│ => { │
│ + tooltip.style.display = "none"; │
│ + }); │
│ + } │
│ +} │
└──────────────────────────────────────────────┘
┌─ web/src/components/PlayerProfile/PlayerEquipment/PlayerEquipment.astro ───┐
│ diff --git a/web/src/components/PlayerProfile/PlayerEquipment/PlayerEquipment.astr │
│ o b/web/src/components/PlayerProfile/PlayerEquipment/PlayerEquipment.astro │
│ index 23e627a..d52ef95 100644 │
│ --- a/web/src/components/PlayerProfile/PlayerEquipment/PlayerEquipment.astro │
│ +++ b/web/src/components/PlayerProfile/PlayerEquipment/PlayerEquipment.astro │
│ @@ -6,6 +6,7 @@ import { │
│ getZamimgIconUrl, │
│ getWowheadUrl, │
│ } from "../../../lib/equipment-utils"; │
│ +import ItemTooltip from "../../ItemTooltip/ItemTooltip.astro"; │
│ import "./PlayerEquipment.scss"; │
│ │
│ interface Props { │
│ @@ -63,9 +64,14 @@ if (equipment && Object.keys(equipment).length > 0) { │
│ <a │
│ href={href} │
│ class={`equip-icon-wrap ${qualityClass}`} │
│ - title={title} │
│ target="_blank" │
│ rel="noopener noreferrer" │
│ + data-item-id={itemId} │
│ + data-item-name={title} │
│ + data-quality={item.quality || "COMMON"} │
│ + data-enchants={JSON.stringify(item.enchantments || [])} │
│ + data-scaling={JSON.stringify(item.scaling_options || {})} │
│ + data-spell-description={item.spell_description || undefined} │
│ > │
│ <div class="equip-icon-inner"> │
│ {iconUrl ? ( │
│ @@ -105,4 +111,5 @@ if (equipment && Object.keys(equipment).length > 0) { │
│ ) │
│ } │
│ </div> │
│ + <ItemTooltip /> │
│ </div> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...uipment/PlayerEquipment.astro ───┐
│ diff --git a/web/src/components/PlayerProfil │
│ e/PlayerEquipment/PlayerEquipment.astro b/we │
│ b/src/components/PlayerProfile/PlayerEquipme │
│ nt/PlayerEquipment.astro │
│ index 23e627a..d52ef95 100644 │
│ --- a/web/src/components/PlayerProfile/Playe │
│ rEquipment/PlayerEquipment.astro │
│ +++ b/web/src/components/PlayerProfile/Playe │
│ rEquipment/PlayerEquipment.astro │
│ @@ -6,6 +6,7 @@ import { │
│ getZamimgIconUrl, │
│ getWowheadUrl, │
│ } from "../../../lib/equipment-utils"; │
│ +import ItemTooltip from "../../ItemTooltip/ │
│ ItemTooltip.astro"; │
│ import "./PlayerEquipment.scss"; │
│ │
│ interface Props { │
│ @@ -63,9 +64,14 @@ if (equipment && Object.k │
│ eys(equipment).length > 0) { │
│ <a │
│ href={href} │
│ class={`equip-icon-wrap $ │
│ {qualityClass}`} │
│ - title={title} │
│ target="_blank" │
│ rel="noopener noreferrer" │
│ + data-item-id={itemId} │
│ + data-item-name={title} │
│ + data-quality={item.qualit │
│ y || "COMMON"} │
│ + data-enchants={JSON.strin │
│ gify(item.enchantments || [])} │
│ + data-scaling={JSON.string │
│ ify(item.scaling_options || {})} │
│ + data-spell-description={i │
│ tem.spell_description || undefined} │
│ > │
│ <div class="equip-icon-in │
│ ner"> │
│ {iconUrl ? ( │
│ @@ -105,4 +111,5 @@ if (equipment && Object. │
│ keys(equipment).length > 0) { │
│ ) │
│ } │
│ </div> │
│ + <ItemTooltip /> │
│ </div> │
└──────────────────────────────────────────────┘
┌─ web/src/lib/types.ts ─────────────────────────────────────────────────────┐
│ diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts │
│ index 15ed8d5..7dabde9 100644 │
│ --- a/web/src/lib/types.ts │
│ +++ b/web/src/lib/types.ts │
│ @@ -75,6 +75,13 @@ export interface Equipment { │
│ [slot: string]: EquipmentItem; │
│ } │
│ │
│ +export interface ScalingOption { │
│ + stats?: Record<string, number>; │
│ + ilvl?: number; │
│ + weaponDamageMin?: number; │
│ + weaponDamageMax?: number; │
│ +} │
│ + │
│ export interface EquipmentItem { │
│ id: number; │
│ slot_type: string; │
│ @@ -83,6 +90,7 @@ export interface EquipmentItem { │
│ quality: string; │
│ item_icon_slug?: string; │
│ enchantments?: Enchantment[]; │
│ + scaling_options?: Record<string, ScalingOption>; │
│ } │
│ │
│ export interface Enchantment { │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ web/src/lib/types.ts ───────────────┐
│ diff --git a/web/src/lib/types.ts b/web/src/ │
│ lib/types.ts │
│ index 15ed8d5..7dabde9 100644 │
│ --- a/web/src/lib/types.ts │
│ +++ b/web/src/lib/types.ts │
│ @@ -75,6 +75,13 @@ export interface Equipmen │
│ t { │
│ [slot: string]: EquipmentItem; │
│ } │
│ │
│ +export interface ScalingOption { │
│ + stats?: Record<string, number>; │
│ + ilvl?: number; │
│ + weaponDamageMin?: number; │
│ + weaponDamageMax?: number; │
│ +} │
│ + │
│ export interface EquipmentItem { │
│ id: number; │
│ slot_type: string; │
│ @@ -83,6 +90,7 @@ export interface Equipment │
│ Item { │
│ quality: string; │
│ item_icon_slug?: string; │
│ enchantments?: Enchantment[]; │
│ + scaling_options?: Record<string, ScalingO │
│ ption>; │
│ } │
│ │
│ export interface Enchantment { │
└──────────────────────────────────────────────┘
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET