Add search button with modal
- Create SearchModal.vue with text field and results display - Add Search button to extraButtons.vue - Uses /find API endpoint for server-side search - Modal matches existing app styling Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<transition name="modalAni">
|
||||
<div v-if="show" class="modal" @click="checkIfClickShouldCloseModal">
|
||||
<div class="modalContainer">
|
||||
<div class="modalWrapper">
|
||||
<span class="closeModalImg" @click="closeModal">×</span>
|
||||
<h1>Search Games</h1>
|
||||
<div class="searchContainer">
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchTerm"
|
||||
@input="debouncedSearch"
|
||||
placeholder="Search for games..."
|
||||
class="searchInput"
|
||||
/>
|
||||
<div v-if="searchTerm.length > 0" class="searchResults">
|
||||
<div v-if="loading" class="loading">Searching...</div>
|
||||
<div v-else-if="results.length > 0" class="resultsList">
|
||||
<div v-for="game in results" :key="game" class="resultItem">
|
||||
{{ game }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="noResults">No results found</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
searchTerm: "",
|
||||
results: [],
|
||||
loading: false,
|
||||
searchTimeout: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
this.show = true;
|
||||
this.searchTerm = "";
|
||||
this.results = [];
|
||||
},
|
||||
closeModal() {
|
||||
this.show = false;
|
||||
this.searchTerm = "";
|
||||
this.results = [];
|
||||
this.loading = false;
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
this.searchTimeout = null;
|
||||
}
|
||||
},
|
||||
checkIfClickShouldCloseModal(event) {
|
||||
if (event.target.classList[0] === "modal") {
|
||||
this.closeModal();
|
||||
}
|
||||
},
|
||||
debouncedSearch() {
|
||||
// Clear existing timeout
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
// Set new timeout
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.performSearch();
|
||||
}, 250);
|
||||
},
|
||||
async performSearch() {
|
||||
if (this.searchTerm.length === 0) {
|
||||
this.results = [];
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await this.axios({
|
||||
method: "post",
|
||||
url: `${window.__RUNTIME_CONFIG__.API_HOSTNAME}/find`,
|
||||
data: new URLSearchParams({ search_term: this.searchTerm }),
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
});
|
||||
// Parse games from HTML response
|
||||
this.results = this.parseGamesFromResponse(response.data);
|
||||
} catch (error) {
|
||||
console.error("Error searching games:", error);
|
||||
this.results = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
parseGamesFromResponse(htmlData) {
|
||||
// The server returns HTML from FoundGames templ component
|
||||
// It generates divs containing game names in <p> tags
|
||||
const games = [];
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlData, "text/html");
|
||||
|
||||
// Find all p tags inside divs (the templ generates <div><p>{game}</p></div>)
|
||||
const pTags = doc.querySelectorAll("div p");
|
||||
for (const p of pTags) {
|
||||
const gameName = p.textContent.trim();
|
||||
if (gameName) {
|
||||
games.push(gameName);
|
||||
}
|
||||
}
|
||||
|
||||
return games;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.searchContainer {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 1.1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.searchInput:focus {
|
||||
outline: none;
|
||||
border-color: #ff9c00;
|
||||
box-shadow: 0 0 5px rgba(255, 156, 0, 0.3);
|
||||
}
|
||||
|
||||
.searchResults {
|
||||
margin-top: 15px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.resultsList {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.resultItem {
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
background-color: #f8f8f8;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.resultItem:hover {
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
.noResults {
|
||||
color: #666;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1000px) {
|
||||
.searchInput {
|
||||
font-size: 1rem;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.resultItem {
|
||||
padding: 10px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,20 +4,24 @@
|
||||
<button @click="resetPoints">Reset points</button>
|
||||
<button @click="handleSyncButtonClick">Sync games</button>
|
||||
<button @click="startSoundTest">Sound test</button>
|
||||
<button @click="showSearchModal">Search</button>
|
||||
<sync-progress-modal
|
||||
:isOpen="showSyncModal"
|
||||
:syncInProgress="syncInProgress"
|
||||
@close="handleSyncComplete"
|
||||
></sync-progress-modal>
|
||||
<search-modal ref="searchModal"></search-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SyncProgressModal from "./SyncProgressModal.vue";
|
||||
import SearchModal from "./SearchModal.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SyncProgressModal,
|
||||
SearchModal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -94,6 +98,9 @@ export default {
|
||||
startSoundTest() {
|
||||
this.$emit("start-sound-test");
|
||||
},
|
||||
showSearchModal() {
|
||||
this.$refs.searchModal.openModal();
|
||||
},
|
||||
APIresetPlaylist() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axios({
|
||||
|
||||
Reference in New Issue
Block a user