2026-08-03 04:23:33 +02:00
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, onMounted } from 'vue'
|
|
|
|
|
|
import { useAuth } from '~/composables/useAuth.js'
|
|
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
isAuthenticated,
|
|
|
|
|
|
isInitialized,
|
|
|
|
|
|
userProfile,
|
2026-08-03 05:39:08 +02:00
|
|
|
|
initializeAuth,
|
2026-08-03 04:23:33 +02:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 06:36:58 +02:00
|
|
|
|
const navigateToUser = (pid) => {
|
|
|
|
|
|
if (!pid) return
|
|
|
|
|
|
navigateTo(`/users/${pid}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-03 04:23:33 +02:00
|
|
|
|
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 () => {
|
2026-08-03 05:39:08 +02:00
|
|
|
|
await initializeAuth({ redirectTo: '/oauth' })
|
2026-08-03 04:23:33 +02:00
|
|
|
|
})
|
|
|
|
|
|
</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"
|
2026-08-05 06:36:58 +02:00
|
|
|
|
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)"
|
2026-08-03 04:23:33 +02:00
|
|
|
|
>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-08-05 06:36:58 +02:00
|
|
|
|
<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>
|
2026-08-03 04:23:33 +02:00
|
|
|
|
</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>
|
2026-08-05 06:36:58 +02:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.user-card:hover {
|
|
|
|
|
|
background: #161b22 !important;
|
|
|
|
|
|
border-color: #58a6ff !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|