mirror of
https://github.com/ryankazokas/turbovault-app.git
synced 2026-04-17 00:32:53 +00:00
Moving to github
This commit is contained in:
9
app/javascript/controllers/application.js
Normal file
9
app/javascript/controllers/application.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Application } from "@hotwired/stimulus"
|
||||
|
||||
const application = Application.start()
|
||||
|
||||
// Configure Stimulus development experience
|
||||
application.debug = true
|
||||
window.Stimulus = application
|
||||
|
||||
export { application }
|
||||
7
app/javascript/controllers/hello_controller.js
Normal file
7
app/javascript/controllers/hello_controller.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
this.element.textContent = "Hello World!"
|
||||
}
|
||||
}
|
||||
282
app/javascript/controllers/igdb_search_controller.js
Normal file
282
app/javascript/controllers/igdb_search_controller.js
Normal file
@@ -0,0 +1,282 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
4
app/javascript/controllers/index.js
Normal file
4
app/javascript/controllers/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// Import and register all your controllers from the importmap via controllers/**/*_controller
|
||||
import { application } from "controllers/application"
|
||||
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
|
||||
eagerLoadControllersFrom("controllers", application)
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["input", "results"]
|
||||
static values = { url: String }
|
||||
|
||||
connect() {
|
||||
this.timeout = null
|
||||
console.log("Location autocomplete connected")
|
||||
}
|
||||
|
||||
search() {
|
||||
clearTimeout(this.timeout)
|
||||
|
||||
const query = this.inputTarget.value.trim()
|
||||
|
||||
if (query.length < 1) {
|
||||
this.hideResults()
|
||||
return
|
||||
}
|
||||
|
||||
this.timeout = setTimeout(() => {
|
||||
this.performSearch(query)
|
||||
}, 200)
|
||||
}
|
||||
|
||||
async performSearch(query) {
|
||||
const url = `${this.urlValue}?q=${encodeURIComponent(query)}`
|
||||
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
const locations = await response.json()
|
||||
this.displayResults(locations)
|
||||
} catch (error) {
|
||||
console.error("Location search error:", error)
|
||||
}
|
||||
}
|
||||
|
||||
displayResults(locations) {
|
||||
if (locations.length === 0) {
|
||||
this.hideResults()
|
||||
return
|
||||
}
|
||||
|
||||
const html = locations.map(location => `
|
||||
<div class="px-4 py-2 hover:bg-indigo-50 cursor-pointer text-sm"
|
||||
data-action="click->location-autocomplete#selectLocation"
|
||||
data-location="${this.escapeHtml(location)}">
|
||||
${this.escapeHtml(location)}
|
||||
</div>
|
||||
`).join('')
|
||||
|
||||
this.resultsTarget.innerHTML = html
|
||||
this.resultsTarget.classList.remove("hidden")
|
||||
}
|
||||
|
||||
selectLocation(event) {
|
||||
const location = event.currentTarget.dataset.location
|
||||
this.inputTarget.value = location
|
||||
this.hideResults()
|
||||
}
|
||||
|
||||
hideResults() {
|
||||
if (this.hasResultsTarget) {
|
||||
this.resultsTarget.classList.add("hidden")
|
||||
}
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div')
|
||||
div.textContent = text
|
||||
return div.innerHTML
|
||||
}
|
||||
|
||||
// Hide results when clicking outside
|
||||
clickOutside(event) {
|
||||
if (!this.element.contains(event.target)) {
|
||||
this.hideResults()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user