mirror of
https://github.com/ryankazokas/turbovault-app.git
synced 2026-04-16 22:12:53 +00:00
283 lines
8.0 KiB
JavaScript
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()
|
|
}
|
|
}
|
|
}
|