feat(item-card): add 'market'/'catalog' mode prop with defensive price checks

Introduces optional `mode` prop ("market"/"catalog") to conditionally render components like rarity badges. Adds defensive checks for missing price data and priceHistory properties to prevent runtime errors. New icons `Badge` and `Boxes` imported from lucide-react.
This commit is contained in:
Evandro Fonseca Junior committed 2025-10-30 14:10:46 -03:00
1 parent 29ba5aece0
commit 44d7f8ce81
21 files changed
+1181 -714

No files matched your search

+114 -19
View File
@@ -55,6 +55,18 @@ class SeleniumSpider:
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument('--window-size=1920,1080')
# Desabilita imagens para acelerar carregamento
chrome_prefs = {
'profile.managed_default_content_settings.images': 2
}
chrome_options.add_experimental_option('prefs', chrome_prefs)
# Estratégia de carregamento mais rápida
try:
chrome_options.page_load_strategy = 'eager'
except Exception:
pass
# User agent
chrome_options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36')
@@ -92,20 +104,17 @@ class SeleniumSpider:
self.driver.quit()
logger.info("Driver fechado")
def get_page(self, url, wait_time=3):
def get_page(self, url, wait_time=0):
"""
Carrega uma página e aguarda carregamento
"""
logger.info(f"Carregando página: {url}")
self.driver.get(url)
# Aguarda um tempo para a página carregar
time.sleep(wait_time)
# Aguarda elementos principais carregarem
try:
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.TAG_NAME, "body"))
EC.presence_of_element_located((By.TAG_NAME, "table"))
)
except Exception as e:
logger.warning(f"Timeout esperando elementos: {e}")
@@ -113,7 +122,7 @@ class SeleniumSpider:
def get_categories(self):
"""Extrai links das categorias da página principal"""
url = "https://www.tibiawiki.com.br/wiki/Itens"
self.get_page(url, wait_time=4)
self.get_page(url, wait_time=0)
# Obtém HTML após renderização
html = self.driver.page_source
@@ -153,7 +162,7 @@ class SeleniumSpider:
logger.info(f"Processando categoria: {category_name}")
logger.info(f"{'='*80}")
self.get_page(url, wait_time=3)
self.get_page(url, wait_time=0)
# Obtém HTML após renderização
html = self.driver.page_source
@@ -194,24 +203,105 @@ class SeleniumSpider:
if not cells:
continue
# Destacar visualmente a linha atual no navegador
try:
self.driver.execute_script(
"""
(function(tableIndex, rowIndex){
try {
if (!window.__scrapeStyleInjected) {
const style = document.createElement('style');
style.textContent = '.__scrapeHighlight{outline:2px solid #e11d48;background:rgba(225,29,72,0.08);}';
document.head.appendChild(style);
window.__scrapeStyleInjected = true;
}
document.querySelectorAll('tr.__scrapeHighlight').forEach(el=>el.classList.remove('__scrapeHighlight'));
let tables = document.querySelectorAll('table.wikitable');
if (!tables || tables.length===0) tables = document.querySelectorAll('table');
const t = tables[tableIndex];
if (!t) return;
const trs = t.querySelectorAll('tr');
const r = trs[rowIndex+1]; // +1 para ignorar cabeçalho
if (!r) return;
r.classList.add('__scrapeHighlight');
r.scrollIntoView({block:'center'});
} catch(e) {}
})(arguments[0], arguments[1]);
""",
table_idx, row_idx
)
except Exception:
pass
# Processa cada célula
for cell_idx, cell in enumerate(cells):
if cell_idx >= len(headers):
break
header = headers[cell_idx]
header_clean = clean_text(header)
header_key = header_clean.casefold()
cell_text = clean_text(cell.get_text(strip=True))
# Extrai imagem
img = cell.find('img')
if img and img.get('src'):
img_src = img.get('src')
if not img_src.startswith('http'):
img_src = urljoin(url, img_src)
item_data['image_url'] = img_src
# Mapeia coluna para campo
self._map_column_to_field(item_data, header, cell_text)
# Extrai possíveis imagens da célula
imgs = cell.find_all('img')
img_srcs = []
for im in imgs:
src = im.get('src') if im else None
if not src:
continue
if not src.startswith('http'):
src = urljoin(url, src)
img_srcs.append(src)
# 1) Coluna sem título: considerar como ícone do item; ignorar Tick/Cross
if not header_clean:
for img_src in img_srcs:
filename = img_src.split('/')[-1].lower()
if 'tick' in filename or 'cross' in filename:
continue
if 'image_url' not in item_data:
item_data['image_url'] = img_src
break
# nada mais a fazer nesta célula
continue
# 2) Coluna "agrupável": mapear Tick/Cross para 1/0 e não baixar imagem
if 'agrup' in header_key:
assigned = False
for img_src in img_srcs:
filename = img_src.split('/')[-1].lower()
if 'tick' in filename:
item_data['agrupavel'] = 1
assigned = True
break
if 'cross' in filename:
item_data['agrupavel'] = 0
assigned = True
break
if not assigned:
text_key = cell_text.casefold()
if text_key == 'sim':
item_data['agrupavel'] = 1
assigned = True
elif text_key in ('não', 'nao'):
item_data['agrupavel'] = 0
assigned = True
# nunca definir image_url a partir desta coluna
continue
# 3) Demais colunas: mapear campos; só usar imagem se não for Tick/Cross
# Preferir texto para campos conhecidos
self._map_column_to_field(item_data, header_clean, cell_text)
# Considerar imagem apenas se ainda não temos image_url e não for Tick/Cross
if 'image_url' not in item_data and img_srcs:
for img_src in img_srcs:
filename = img_src.split('/')[-1].lower()
if 'tick' in filename or 'cross' in filename:
continue
item_data['image_url'] = img_src
break
# Salva item se tiver nome
if item_data.get('nome'):
@@ -312,6 +402,11 @@ class SeleniumSpider:
if not image_url:
return
# Nunca baixar ícones Tick/Cross
basename = image_url.split('/')[-1].lower()
if 'tick' in basename or 'cross' in basename:
return
categoria = item_data.get('categoria', 'uncategorized')
nome = item_data.get('nome', 'unknown')
@@ -392,8 +487,8 @@ class SeleniumSpider:
try:
self.scrape_category(category_name, url)
# Delay entre categorias
time.sleep(2)
# Delay mínimo entre categorias (reduzido)
time.sleep(0.2)
except Exception as e:
logger.error(f"Erro ao processar {category_name}: {e}")