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="resetPoints">Reset points</button>
|
||||||
<button @click="handleSyncButtonClick">Sync games</button>
|
<button @click="handleSyncButtonClick">Sync games</button>
|
||||||
<button @click="startSoundTest">Sound test</button>
|
<button @click="startSoundTest">Sound test</button>
|
||||||
|
<button @click="showSearchModal">Search</button>
|
||||||
<sync-progress-modal
|
<sync-progress-modal
|
||||||
:isOpen="showSyncModal"
|
:isOpen="showSyncModal"
|
||||||
:syncInProgress="syncInProgress"
|
:syncInProgress="syncInProgress"
|
||||||
@close="handleSyncComplete"
|
@close="handleSyncComplete"
|
||||||
></sync-progress-modal>
|
></sync-progress-modal>
|
||||||
|
<search-modal ref="searchModal"></search-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import SyncProgressModal from "./SyncProgressModal.vue";
|
import SyncProgressModal from "./SyncProgressModal.vue";
|
||||||
|
import SearchModal from "./SearchModal.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
SyncProgressModal,
|
SyncProgressModal,
|
||||||
|
SearchModal,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -94,6 +98,9 @@ export default {
|
|||||||
startSoundTest() {
|
startSoundTest() {
|
||||||
this.$emit("start-sound-test");
|
this.$emit("start-sound-test");
|
||||||
},
|
},
|
||||||
|
showSearchModal() {
|
||||||
|
this.$refs.searchModal.openModal();
|
||||||
|
},
|
||||||
APIresetPlaylist() {
|
APIresetPlaylist() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.axios({
|
this.axios({
|
||||||
|
|||||||
Reference in New Issue
Block a user