mod-portal-v2/app/pages/search/index.vue
2026-08-05 06:36:58 +02:00

217 lines
No EOL
7.5 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,
initializeAuth,
logout
} = useAuth()
const searchQuery = ref('')
const searchResults = ref([])
const isSearching = ref(false)
const hasSearched = ref(false)
const errorMessage = ref('')
const currentPage = ref(1)
const totalPages = ref(0)
const totalResults = ref(0)
const limit = 10
const handleLogout = () => {
logout()
}
const handleNavigate = (path) => {
navigateTo(path)
}
const navigateToUser = (pid) => {
if (!pid) return
navigateTo(`/users/${pid}`)
}
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 () => {
await initializeAuth({ redirectTo: '/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"
class="user-card"
style="display: flex; align-items: center; justify-content: space-between; padding: 0.85rem 1rem; background: #0d1117; border: 1px solid #30363d; border-radius: 8px; cursor: pointer; transition: background 0.15s ease, border-color 0.15s ease;"
@click="navigateToUser(user.pid)"
>
<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 style="display: flex; align-items: center; gap: 1rem;">
<div v-if="user.email" style="font-size: 0.85rem; color: #8b949e;">
{{ user.email }}
</div>
<button
type="button"
style="padding: 0.35rem 0.75rem; background: #21262d; border: 1px solid #30363d; border-radius: 6px; color: #c9d1d9; font-size: 0.8rem; font-weight: 600; pointer-events: none;"
>
View
</button>
</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>
<style scoped>
.user-card:hover {
background: #161b22 !important;
border-color: #58a6ff !important;
}
</style>