Files
turbovault-app/app/javascript/controllers/igdb_search_controller.js
2026-03-28 19:24:29 -04:00

283 lines
8.0 KiB
JavaScript

import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [
"query",
"results",
"igdbId",
"title",
"summary",
"platformSelect",
"customGameToggle",
"igdbSection",
"manualSection"
]
static values = {
url: String
}
connect() {
console.log("IGDB Search controller connected!")
console.log("Search URL:", this.urlValue)
console.log("Query target exists:", this.hasQueryTarget)
console.log("Results target exists:", this.hasResultsTarget)
console.log("Title target exists:", this.hasTitleTarget)
this.timeout = null
this.selectedGame = null
this.searchResults = []
}
search() {
console.log("Search triggered")
clearTimeout(this.timeout)
const query = this.queryTarget.value.trim()
console.log("Query:", query)
if (query.length < 2) {
console.log("Query too short, hiding results")
this.hideResults()
return
}
console.log("Starting search timeout...")
this.timeout = setTimeout(() => {
this.performSearch(query)
}, 300)
}
async performSearch(query) {
const platformId = this.hasPlatformSelectTarget ? this.platformSelectTarget.value : ""
const url = `${this.urlValue}?q=${encodeURIComponent(query)}&platform_id=${platformId}`
console.log("Fetching:", url)
try {
const response = await fetch(url)
console.log("Response status:", response.status)
const results = await response.json()
console.log("Results:", results)
this.displayResults(results)
} catch (error) {
console.error("IGDB search error:", error)
}
}
displayResults(results) {
if (results.length === 0) {
this.resultsTarget.innerHTML = `
<div class="p-4 text-sm text-gray-500 text-center border-t">
No games found. <a href="#" data-action="click->igdb-search#showManualEntry" class="text-indigo-600 hover:text-indigo-800">Add custom game</a>
</div>
`
this.resultsTarget.classList.remove("hidden")
return
}
// Store results in memory and use indices instead of embedding JSON
this.searchResults = results
const html = results.map((game, index) => {
// HTML escape function
const escapeHtml = (text) => {
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
return `
<div class="p-3 hover:bg-gray-50 cursor-pointer border-b last:border-b-0 flex gap-3"
data-action="click->igdb-search#selectGame"
data-index="${index}">
${game.cover_url ? `
<img src="https://images.igdb.com/igdb/image/upload/t_thumb/${game.cover_url}.jpg"
class="w-12 h-16 object-cover rounded"
alt="${escapeHtml(game.name)}">
` : `
<div class="w-12 h-16 bg-gray-200 rounded flex items-center justify-center">
<span class="text-gray-400 text-xs">No<br>Cover</span>
</div>
`}
<div class="flex-1 min-w-0">
<div class="font-semibold text-gray-900 truncate">${escapeHtml(game.name)}</div>
<div class="text-sm text-gray-600">${escapeHtml(game.platform)}${game.year ? `${game.year}` : ''}</div>
${game.genres && game.genres.length > 0 ? `
<div class="flex gap-1 mt-1 flex-wrap">
${game.genres.slice(0, 3).map(genre => `
<span class="px-1.5 py-0.5 bg-indigo-100 text-indigo-700 text-xs rounded">${escapeHtml(genre)}</span>
`).join('')}
</div>
` : ''}
</div>
<div class="text-right">
<span class="text-xs text-gray-500">${Math.round(game.confidence)}% match</span>
</div>
</div>
`
}).join('')
this.resultsTarget.innerHTML = html
this.resultsTarget.classList.remove("hidden")
}
selectGame(event) {
const index = parseInt(event.currentTarget.dataset.index)
const gameData = this.searchResults[index]
this.selectedGame = gameData
console.log("Selected game:", gameData)
// Fill in the form fields
if (this.hasTitleTarget) {
this.titleTarget.value = gameData.name
console.log("Set title to:", gameData.name)
} else {
console.warn("Title target not found")
}
if (this.hasIgdbIdTarget) {
this.igdbIdTarget.value = gameData.igdb_id
console.log("Set IGDB ID to:", gameData.igdb_id)
} else {
console.warn("IGDB ID target not found")
}
if (this.hasSummaryTarget && gameData.summary) {
this.summaryTarget.value = gameData.summary
console.log("Set summary")
}
// Set genres
if (gameData.genre_ids && gameData.genre_ids.length > 0) {
this.setGenres(gameData.genre_ids)
}
// Update the query field to show selected game
this.queryTarget.value = gameData.name
// Hide results
this.hideResults()
// Show IGDB badge/info
this.showIgdbInfo(gameData)
}
setGenres(genreIds) {
console.log("Setting genres:", genreIds)
// Find all genre checkboxes
const genreCheckboxes = document.querySelectorAll('input[name="game[genre_ids][]"]')
// Uncheck all first
genreCheckboxes.forEach(checkbox => {
checkbox.checked = false
})
// Check the matched genres
genreIds.forEach(genreId => {
const checkbox = document.querySelector(`input[name="game[genre_ids][]"][value="${genreId}"]`)
if (checkbox) {
checkbox.checked = true
console.log("Checked genre:", genreId)
}
})
}
showIgdbInfo(gameData) {
// Add a visual indicator that this is from IGDB
const badge = document.createElement('div')
badge.className = 'mt-2 space-y-2'
let genreText = ''
if (gameData.genres && gameData.genres.length > 0) {
genreText = `
<div class="text-xs text-green-700">
<strong>Genres auto-set:</strong> ${gameData.genres.join(', ')}
</div>
`
}
badge.innerHTML = `
<div class="inline-flex items-center px-2 py-1 bg-green-100 text-green-800 text-xs rounded">
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/>
</svg>
IGDB Match (${Math.round(gameData.confidence)}% confidence)
</div>
${genreText}
`
// Insert after query field
const existingBadge = this.queryTarget.parentElement.querySelector('.igdb-match-badge')
if (existingBadge) {
existingBadge.remove()
}
badge.classList.add('igdb-match-badge')
this.queryTarget.parentElement.appendChild(badge)
}
showManualEntry(event) {
event.preventDefault()
// Clear IGDB fields
if (this.hasIgdbIdTarget) {
this.igdbIdTarget.value = ""
}
// Focus on title field
if (this.hasTitleTarget) {
this.titleTarget.focus()
}
this.hideResults()
// Remove IGDB badge
const badge = this.queryTarget.parentElement.querySelector('.igdb-match-badge')
if (badge) {
badge.remove()
}
}
hideResults() {
if (this.hasResultsTarget) {
this.resultsTarget.classList.add("hidden")
}
}
clearSearch() {
this.queryTarget.value = ""
this.hideResults()
if (this.hasTitleTarget) {
this.titleTarget.value = ""
}
if (this.hasIgdbIdTarget) {
this.igdbIdTarget.value = ""
}
if (this.hasSummaryTarget) {
this.summaryTarget.value = ""
}
// Uncheck all genres
const genreCheckboxes = document.querySelectorAll('input[name="game[genre_ids][]"]')
genreCheckboxes.forEach(checkbox => {
checkbox.checked = false
})
const badge = this.queryTarget.parentElement.querySelector('.igdb-match-badge')
if (badge) {
badge.remove()
}
}
// Hide results when clicking outside
clickOutside(event) {
if (!this.element.contains(event.target)) {
this.hideResults()
}
}
}