mod-portal-v2/app/pages/search/index.vue
2026-08-03 04:23:33 +02:00

207 lines
No EOL
6.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, onMounted } from 'vue'
import { useAuth } from '~/composables/useAuth.js'
const {
isAuthenticated,
isInitialized,
userProfile,
verifySession,
silentRefresh,
logout
} = useAuth()
const searchQuery = ref('')
const searchResults = ref([])
const isSearching = ref(false)
const hasSearched = ref(false)
const errorMessage = ref('')
// pagination state
const currentPage = ref(1)
const totalPages = ref(0)
const totalResults = ref(0)
const limit = 10
const handleLogout = () => {
logout()
}
const handleNavigate = (path) => {
navigateTo(path)
}
const executeSearch = async (page = 1) => {
const query = searchQuery.value.trim()
if (!query) return
isSearching.value = true
hasSearched.value = true
errorMessage.value = ''
try {
const res = await $fetch(`/api/admin/users/search`, {
params: {
q: query,
page,
limit
}
})
searchResults.value = res.data || []
currentPage.value = res.pagination.page
totalPages.value = res.pagination.totalPages
totalResults.value = res.pagination.total
} catch (err) {
errorMessage.value = err.data?.statusMessage || err.data?.message || 'Failed to search database.'
searchResults.value = []
totalPages.value = 0
totalResults.value = 0
} finally {
isSearching.value = false
}
}
const handleSearchSubmit = () => {
currentPage.value = 1
executeSearch(1)
}
const goToPage = (page) => {
if (page < 1 || page > totalPages.value || isSearching.value) return
executeSearch(page)
}
onMounted(async () => {
if (isAuthenticated.value) {
const valid = await verifySession()
if (!valid) {
navigateTo('/oauth')
}
} else {
const success = await silentRefresh()
if (!success) {
navigateTo('/oauth')
}
}
})
</script>
<template>
<div>
<div v-if="!isInitialized" class="global-loader-wrapper">
<div class="spinner lg"></div>
<p style="color: #8b949e; margin-top: 1rem; font-size: 0.95rem;">
Connecting to Keycloak...
</p>
</div>
<div v-else-if="isAuthenticated" class="settings-container">
<Header
:user-profile="userProfile"
active-tab="search"
@logout="handleLogout"
@navigate="handleNavigate"
/>
<main class="settings-main-content">
<section class="settings-panel" style="width: 100%;">
<div class="panel-header archivo-black">
<span>SEARCH USERS</span>
</div>
<div class="panel-body" style="padding: 1.5rem;">
<form @submit.prevent="handleSearchSubmit" style="display: flex; gap: 0.75rem; margin-bottom: 1.5rem;">
<input
v-model="searchQuery"
type="text"
placeholder="Search by SFID, PID, or Email"
required
style="flex: 1; padding: 0.65rem 0.85rem; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #c9d1d9; font-size: 0.95rem; outline: none;"
/>
<button
type="submit"
class="btn-login-action"
style="padding: 0.65rem 1.25rem;"
:disabled="isSearching"
>
<span v-if="isSearching" class="spinner sm btn-spinner"></span>
<span style="vertical-align: middle;">
{{ isSearching ? 'Searching...' : 'Search' }}
</span>
</button>
</form>
<p v-if="errorMessage" style="color: #f85149; font-size: 0.85rem; margin-bottom: 1rem; text-align: center;">
{{ errorMessage }}
</p>
<div v-if="hasSearched && !isSearching && searchResults.length > 0" style="margin-bottom: 1rem; font-size: 0.85rem; color: #8b949e;">
Showing {{ (currentPage - 1) * limit + 1 }}{{ Math.min(currentPage * limit, totalResults) }} of {{ totalResults }} results
</div>
<div v-if="searchResults.length > 0" class="results-list" style="display: flex; flex-direction: column; gap: 0.75rem;">
<div
v-for="user in searchResults"
:key="user.id"
style="display: flex; align-items: center; justify-content: space-between; padding: 0.85rem 1rem; background: #0d1117; border: 1px solid #30363d; border-radius: 8px;"
>
<div style="display: flex; align-items: center; gap: 0.85rem;">
<img
:src="user.pid ? `https://mii.spfn.net/${user.pid}/main.png` : '/img_unknown_MiiIcon.png'"
:alt="user.sfid || 'Mii Avatar'"
style="width: 44px; height: 44px; border-radius: 50%; background: #161b22; object-fit: contain; flex-shrink: 0;"
@error="(e) => e.target.src = '/img_unknown_MiiIcon.png'"
/>
<div style="display: flex; flex-direction: column;">
<span style="font-weight: 600; color: #f0f6fc; font-size: 0.95rem;">
{{ user.sfid || 'No SFID Linked' }}
</span>
<span style="font-size: 0.8rem; color: #8b949e; margin-top: 0.15rem;">
PID: {{ user.pid || 'N/A' }}
</span>
</div>
</div>
<div v-if="user.email" style="font-size: 0.85rem; color: #8b949e;">
{{ user.email }}
</div>
</div>
</div>
<div v-else-if="hasSearched && !isSearching" style="text-align: center; color: #8b949e; padding: 2rem 0; font-size: 0.9rem;">
No users found matching "{{ searchQuery }}"
</div>
<div
v-if="totalPages > 1"
style="display: flex; align-items: center; justify-content: space-between; margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid #30363d;"
>
<button
type="button"
style="padding: 0.5rem 1rem; background: #21262d; border: 1px solid #30363d; border-radius: 6px; color: #c9d1d9; font-size: 0.85rem; cursor: pointer;"
:disabled="currentPage === 1 || isSearching"
@click="goToPage(currentPage - 1)"
>
Previous
</button>
<span style="font-size: 0.85rem; color: #8b949e;">
Page {{ currentPage }} of {{ totalPages }}
</span>
<button
type="button"
style="padding: 0.5rem 1rem; background: #21262d; border: 1px solid #30363d; border-radius: 6px; color: #c9d1d9; font-size: 0.85rem; cursor: pointer;"
:disabled="currentPage === totalPages || isSearching"
@click="goToPage(currentPage + 1)"
>
Next
</button>
</div>
</div>
</section>
</main>
</div>
</div>
</template>