Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 04e11e8120 | |||
| 66287a84c6 |
@@ -25,6 +25,7 @@ import ChooseDeck from "./pages/Game/ChooseDeck"
|
||||
import PlayerSetup from "./pages/Game/PlayerSetup"
|
||||
import GameModalsDemo from "./pages/Game/GameModalsDemo"
|
||||
import { GameWebSocketProvider } from "./contexts/GameWebSocketContext"
|
||||
import Admin from "./pages/Admin/Admin"
|
||||
|
||||
function App() {
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
@@ -78,6 +79,9 @@ function App() {
|
||||
<Route path={ROUTES.REPORTS} element={<Reports />} />
|
||||
<Route path={ROUTES.CHOOSE_DECK} element={<ChooseDeck />} />
|
||||
<Route path={ROUTES.PLAYER_SETUP} element={<PlayerSetup />} />
|
||||
<Route path={ROUTES.ADMIN} element={<Admin />} />
|
||||
|
||||
|
||||
</Routes>
|
||||
</Router>
|
||||
</GameWebSocketProvider>
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
// ...new file...
|
||||
import { apiClient } from "./userApi"
|
||||
|
||||
/**
|
||||
* Ellenőrzi localStorage.authLevel === "1". Ha nem, redirect a megadott útvonalra.
|
||||
* Visszatér: true (admin) | false (nem admin, redirect történt)
|
||||
*/
|
||||
export function ensureAdminOrRedirect(redirectPath = "/") {
|
||||
try {
|
||||
const level = localStorage.getItem("authLevel")
|
||||
const isAdmin = String(level) === "1"
|
||||
if (!isAdmin) {
|
||||
window.location.replace(redirectPath)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} catch (err) {
|
||||
window.location.replace(redirectPath)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin: paginált paklik lekérése (admin endpoint)
|
||||
* Visszatér: tömb of decks (raw vagy {decks: []} formátum kezelve)
|
||||
*/
|
||||
export async function adminListDecks(from = 0, to = 99) {
|
||||
if (!ensureAdminOrRedirect("/")) return []
|
||||
try {
|
||||
const resp = await apiClient.get(`/admin/decks/page/${from}/${to}`)
|
||||
const data = resp?.data
|
||||
return data?.decks || data || []
|
||||
} catch (err) {
|
||||
console.error("adminApi.adminListDecks error:", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin: keresés admin végponton.
|
||||
* Ha nincs külön kereső végpont, a backend/resp lehet tömb vagy {decks: []}
|
||||
* Visszatér: tömb of decks
|
||||
*/
|
||||
export async function adminSearchDecks(query = "", limit = 100, offset = 0) {
|
||||
if (!ensureAdminOrRedirect("/")) return []
|
||||
try {
|
||||
if (!query) {
|
||||
// fallback: list page
|
||||
return await adminListDecks(offset, offset + (limit - 1))
|
||||
}
|
||||
// Preferált: admin search endpoint /admin/decks/search/{term}
|
||||
const resp = await apiClient.get(`/admin/decks/search/${encodeURIComponent(query)}`, {
|
||||
params: { limit, offset }
|
||||
})
|
||||
const data = resp?.data
|
||||
return data?.decks || data || []
|
||||
} catch (err) {
|
||||
console.error("adminApi.adminSearchDecks error:", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin: paginált felhasználók lekérése
|
||||
* Visszatér: { users: [], pagination: {...} } vagy tömb of users
|
||||
*/
|
||||
export async function adminListUsers(from = 0, to = 99, includeDeleted = false) {
|
||||
if (!ensureAdminOrRedirect("/")) return { users: [], pagination: { from, to, returned: 0, totalCount: 0, includeDeleted } }
|
||||
try {
|
||||
const resp = await apiClient.get(`/admin/users/page/${from}/${to}`, {
|
||||
params: { includeDeleted }
|
||||
})
|
||||
return resp?.data || { users: [] }
|
||||
} catch (err) {
|
||||
console.error("adminApi.adminListUsers error:", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin: Get user by id
|
||||
*/
|
||||
export async function adminGetUserById(userId, includeDeleted = false) {
|
||||
if (!ensureAdminOrRedirect("/")) return null
|
||||
try {
|
||||
const resp = await apiClient.get(`/admin/users/${userId}`, { params: { includeDeleted } })
|
||||
return resp?.data || null
|
||||
} catch (err) {
|
||||
console.error("adminApi.adminGetUserById error:", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin: Search users
|
||||
*/
|
||||
export async function adminSearchUsers(query = "", includeDeleted = false, limit = 100, offset = 0) {
|
||||
if (!ensureAdminOrRedirect("/")) return []
|
||||
try {
|
||||
if (!query) {
|
||||
// fallback: list page
|
||||
const to = offset + (limit - 1)
|
||||
const res = await adminListUsers(offset, to, includeDeleted)
|
||||
// adminListUsers may return object or array
|
||||
if (Array.isArray(res)) return res
|
||||
return res.users || []
|
||||
}
|
||||
const resp = await apiClient.get(`/admin/users/search/${encodeURIComponent(query)}`, {
|
||||
params: { includeDeleted, limit, offset }
|
||||
})
|
||||
const data = resp?.data
|
||||
// prefer array but handle wrapped
|
||||
return data?.users || data || []
|
||||
} catch (err) {
|
||||
console.error("adminApi.adminSearchUsers error:", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin: Update user
|
||||
*/
|
||||
export async function adminUpdateUser(userId, payload = {}) {
|
||||
if (!ensureAdminOrRedirect("/")) return null
|
||||
try {
|
||||
const resp = await apiClient.patch(`/admin/users/${userId}`, payload)
|
||||
return resp?.data || null
|
||||
} catch (err) {
|
||||
console.error("adminApi.adminUpdateUser error:", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin: Deactivate user
|
||||
*/
|
||||
export async function adminDeactivateUser(userId) {
|
||||
if (!ensureAdminOrRedirect("/")) return null
|
||||
try {
|
||||
const resp = await apiClient.post(`/admin/users/${userId}/deactivate`)
|
||||
return resp?.data || null
|
||||
} catch (err) {
|
||||
console.error("adminApi.adminDeactivateUser error:", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin: Delete user
|
||||
*/
|
||||
export async function adminDeleteUser(userId) {
|
||||
if (!ensureAdminOrRedirect("/")) return null
|
||||
try {
|
||||
const resp = await apiClient.delete(`/admin/users/${userId}`)
|
||||
return resp?.data || null
|
||||
} catch (err) {
|
||||
console.error("adminApi.adminDeleteUser error:", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
ensureAdminOrRedirect,
|
||||
adminListDecks,
|
||||
adminSearchDecks,
|
||||
// new user API exports
|
||||
adminListUsers,
|
||||
adminGetUserById,
|
||||
adminSearchUsers,
|
||||
adminUpdateUser,
|
||||
adminDeactivateUser,
|
||||
adminDeleteUser,
|
||||
}
|
||||
@@ -107,3 +107,59 @@ export const resetPassword = async (token, newPassword) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Add: response interceptor + error parser
|
||||
apiClient.interceptors.response.use(
|
||||
(resp) => resp,
|
||||
(error) => {
|
||||
// Log useful debug info
|
||||
try {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("API Error:", {
|
||||
message: error.message,
|
||||
status: error?.response?.status,
|
||||
url: error?.config?.url,
|
||||
data: error?.response?.data,
|
||||
})
|
||||
} catch (e) {}
|
||||
// forward
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Normalize axios error into readable string
|
||||
* Prefer backend-provided message (resp.data.message), fallback to resp.data or error.message.
|
||||
*/
|
||||
export function getApiErrorMessage(error) {
|
||||
if (!error) return "Ismeretlen hiba"
|
||||
// axios error with response
|
||||
const resp = error.response
|
||||
if (resp) {
|
||||
const data = resp.data
|
||||
if (data) {
|
||||
if (typeof data === "string") return data
|
||||
if (data.message) return String(data.message)
|
||||
// sometimes backend sends { errors: [...] }
|
||||
if (data.errors && Array.isArray(data.errors)) return data.errors.map(e => e.message || JSON.stringify(e)).join("; ")
|
||||
// fallback to whole payload
|
||||
try { return JSON.stringify(data) } catch (e) {}
|
||||
}
|
||||
return `HTTP ${resp.status} ${resp.statusText || ""}`.trim()
|
||||
}
|
||||
// non-response axios error
|
||||
return error.message || String(error)
|
||||
}
|
||||
|
||||
export default {
|
||||
login,
|
||||
register,
|
||||
getUserStats,
|
||||
verifyEmail,
|
||||
getUserProfile,
|
||||
updateUserProfile,
|
||||
deleteUserProfile,
|
||||
forgotPassword,
|
||||
resetPassword,
|
||||
apiClient,
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from "react"
|
||||
import Logo from "../../assets/pictures/Logo"
|
||||
import { Link } from "react-router-dom"
|
||||
import HandleNavigate from "../../utils/HandleNavigate/HandleNavigate" // ✅ importáld a navigációs hookot
|
||||
import { FaSignOutAlt, FaChartBar, FaUser, FaBars } from "react-icons/fa"
|
||||
import { FaSignOutAlt, FaChartBar, FaUser, FaBars, FaUserShield } from "react-icons/fa"
|
||||
|
||||
const navLinkClass = "px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10"
|
||||
const navLinkClassPlay =
|
||||
@@ -13,6 +13,9 @@ const Navbar = () => {
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false)
|
||||
const isLoggedIn = Boolean(localStorage.getItem("authLevel") && localStorage.getItem("username"))
|
||||
|
||||
// Új: ellenőrizzük, hogy admin-e (authLevel === "1")
|
||||
const isAdmin = localStorage.getItem("authLevel") === "1"
|
||||
|
||||
// ✅ Használjuk a HandleNavigate hookot
|
||||
const { goLanding, goAbout, goHome, goLogin, goContacts } = HandleNavigate()
|
||||
|
||||
@@ -111,6 +114,21 @@ const Navbar = () => {
|
||||
<FaChartBar className="w-4 h-4" />
|
||||
<span className="text-sm">Statisztikák</span>
|
||||
</button>
|
||||
|
||||
{/* Admin gomb a felhasználói lenyílóban, csak adminoknak */}
|
||||
{isAdmin && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setUserMenuOpen(false)
|
||||
window.location.href = "/admin"
|
||||
}}
|
||||
className="w-full flex items-center gap-3 px-4 py-2 hover:bg-white/10 text-white"
|
||||
>
|
||||
<FaUserShield className="w-4 h-4" />
|
||||
<span className="text-sm">Admin</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setUserMenuOpen(false)
|
||||
@@ -201,11 +219,26 @@ const Navbar = () => {
|
||||
</button>
|
||||
|
||||
{isLoggedIn && (
|
||||
<Link to="/decks" onClick={() => setMenuOpen(false)} className="px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10 block text-center">
|
||||
<Link
|
||||
to="/decks"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className="px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10 block text-center"
|
||||
>
|
||||
Paklik
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Admin gomb csak authLevel === "1" esetén */}
|
||||
{isAdmin && (
|
||||
<Link
|
||||
to="/admin"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className="px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10 block text-center"
|
||||
>
|
||||
Admin
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
goHome()
|
||||
|
||||
@@ -0,0 +1,750 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import Navbar from "../../components/Navbar/Navbar.jsx"
|
||||
import Footer from "../../components/Footer/Footer.jsx"
|
||||
import Background from "../../assets/backgrounds/Background.jsx"
|
||||
import ButtonGreen from "../../components/Buttons/ButtonGreen.jsx"
|
||||
import { FaLayerGroup, FaUser, FaFilter, FaCalendarAlt, FaArrowUp, FaArrowDown, FaSortAlphaDown, FaSortAlphaUp, FaQuestionCircle, FaPlus, FaChevronLeft, FaChevronRight } from "react-icons/fa"
|
||||
import { motion } from "framer-motion"
|
||||
import {
|
||||
ensureAdminOrRedirect,
|
||||
adminListDecks,
|
||||
adminSearchDecks,
|
||||
adminListUsers,
|
||||
adminGetUserById,
|
||||
adminSearchUsers,
|
||||
adminUpdateUser,
|
||||
adminDeactivateUser,
|
||||
adminDeleteUser
|
||||
} from "../../api/adminApi"
|
||||
import { getApiErrorMessage } from "../../api/userApi"
|
||||
// --- Új importok: navigáció + SearchBox + PopUp + DeckInfoPopUp + deckApi használat ---
|
||||
import HandleNavigate from "../../utils/HandleNavigate/HandleNavigate"
|
||||
import SearchBox from "../../components/Search/SearchBox"
|
||||
import PopUp from "../../components/PopUp/PopUp"
|
||||
import DeckInfoPopUp from "../../components/PopUp/DeckInfoPopUp"
|
||||
|
||||
export default function Admin() {
|
||||
const [active, setActive] = useState("decks") // "decks" | "users"
|
||||
|
||||
// --- Új: navigáció hook ---
|
||||
const { goDeckCreator } = HandleNavigate()
|
||||
|
||||
// --- Szűrés / lista állapotok (DeckManager-ről átemelve) ---
|
||||
const deckTypes = [
|
||||
{ label: "Luck", color: "var(--color-luck)" },
|
||||
{ label: "Question", color: "var(--color-question)" },
|
||||
{ label: "Joker", color: "var(--color-fun)" },
|
||||
]
|
||||
const origins = ["Mind", "Vállalati", "Saját"]
|
||||
|
||||
const [query, setQuery] = useState("")
|
||||
const [allDecks, setAllDecks] = useState([]) // összes betöltött pakli (backend)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectedType, setSelectedType] = useState("All")
|
||||
const [selectedOrigin, setSelectedOrigin] = useState("Mind")
|
||||
const [sortBy, setSortBy] = useState("date-desc")
|
||||
const [showSortHelp, setShowSortHelp] = useState(false)
|
||||
const [selectedDeck, setSelectedDeck] = useState(null)
|
||||
const [itemsPerPage, setItemsPerPage] = useState(20)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
|
||||
// --- User management states ---
|
||||
const [users, setUsers] = useState([])
|
||||
const [userQuery, setUserQuery] = useState("")
|
||||
const [userLoading, setUserLoading] = useState(false)
|
||||
const [selectedUser, setSelectedUser] = useState(null)
|
||||
const [userItemsPerPage, setUserItemsPerPage] = useState(20)
|
||||
const [userCurrentPage, setUserCurrentPage] = useState(1)
|
||||
const [userTotalCount, setUserTotalCount] = useState(0)
|
||||
const [includeDeletedUsers, setIncludeDeletedUsers] = useState(false)
|
||||
// track latest search to avoid race
|
||||
const [userSearchToken, setUserSearchToken] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
ensureAdminOrRedirect("/")
|
||||
let mounted = true
|
||||
const load = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await adminListDecks(0, 99)
|
||||
if (!mounted) return
|
||||
// admin endpoints may already return array of deck objects or {decks:[...]}
|
||||
const rawDecks = Array.isArray(result) ? result : (result.decks || result)
|
||||
const mapped = (rawDecks || []).map(d => ({
|
||||
id: d.id,
|
||||
name: d.name || "",
|
||||
type: d.type === 2 ? "Question" : d.type === 1 ? "Joker" : "Luck",
|
||||
created: d.creationdate ? new Date(d.creationdate).toLocaleDateString() : "",
|
||||
origin: d.ctype === 2 ? "Vállalati" : d.ctype === 0 ? "Mind" : "Saját",
|
||||
raw: d
|
||||
}))
|
||||
setAllDecks(mapped)
|
||||
} catch (err) {
|
||||
console.error("Admin: failed to load decks (admin API)", err)
|
||||
setAllDecks([])
|
||||
} finally {
|
||||
if (mounted) setLoading(false)
|
||||
}
|
||||
}
|
||||
load()
|
||||
return () => { mounted = false }
|
||||
}, [])
|
||||
|
||||
// Debounced search using adminSearchDecks
|
||||
useEffect(() => {
|
||||
const t = setTimeout(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
if (!query) {
|
||||
// if empty, reload full admin list (or keep cached allDecks)
|
||||
const res = await adminListDecks(0, 99)
|
||||
const raw = Array.isArray(res) ? res : (res.decks || res)
|
||||
const mapped = (raw || []).map(d => ({
|
||||
id: d.id,
|
||||
name: d.name || "",
|
||||
type: d.type === 2 ? "Question" : d.type === 1 ? "Joker" : "Luck",
|
||||
created: d.creationdate ? new Date(d.creationdate).toLocaleDateString() : "",
|
||||
origin: d.ctype === 2 ? "Vállalati" : d.ctype === 0 ? "Mind" : "Saját",
|
||||
raw: d
|
||||
}))
|
||||
setAllDecks(mapped)
|
||||
} else {
|
||||
const res = await adminSearchDecks(query, 100, 0)
|
||||
const raw = Array.isArray(res) ? res : (res.decks || res)
|
||||
const mapped = (raw || []).map(d => ({
|
||||
id: d.id,
|
||||
name: d.name || "",
|
||||
type: d.type === 2 ? "Question" : d.type === 1 ? "Joker" : "Luck",
|
||||
created: d.creationdate ? new Date(d.creationdate).toLocaleDateString() : "",
|
||||
origin: d.ctype === 2 ? "Vállalati" : d.ctype === 0 ? "Mind" : "Saját",
|
||||
raw: d
|
||||
}))
|
||||
setAllDecks(mapped)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Admin search error (admin API):", err)
|
||||
setAllDecks([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, 300)
|
||||
return () => clearTimeout(t)
|
||||
}, [query])
|
||||
|
||||
// Load users when admin panel opened or includeDeleted changes
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
const loadUsers = async () => {
|
||||
if (active !== "users") return
|
||||
setUserLoading(true)
|
||||
try {
|
||||
const res = await adminListUsers(0, 99, includeDeletedUsers)
|
||||
// res may be { users: [], pagination: {} } or array
|
||||
const raw = Array.isArray(res) ? res : (res.users || [])
|
||||
if (!mounted) return
|
||||
setUsers((raw || []).map(u => ({
|
||||
id: u.id,
|
||||
username: u.username || u.name || "",
|
||||
email: u.email || "",
|
||||
created: u.creationdate ? new Date(u.creationdate).toLocaleDateString() : "",
|
||||
deleted: !!u.deleted,
|
||||
raw: u
|
||||
})))
|
||||
// set total if pagination provided
|
||||
if (!Array.isArray(res) && res.pagination && typeof res.pagination.totalCount === "number") {
|
||||
setUserTotalCount(res.pagination.totalCount)
|
||||
} else {
|
||||
setUserTotalCount((raw || []).length)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Admin: failed to load users (admin API)", err)
|
||||
setUsers([])
|
||||
setUserTotalCount(0)
|
||||
} finally {
|
||||
if (mounted) setUserLoading(false)
|
||||
}
|
||||
}
|
||||
loadUsers()
|
||||
return () => { mounted = false }
|
||||
}, [active, includeDeletedUsers])
|
||||
|
||||
// Debounced user search
|
||||
useEffect(() => {
|
||||
const token = userSearchToken + 1
|
||||
setUserSearchToken(token)
|
||||
const t = setTimeout(async () => {
|
||||
if (active !== "users") return
|
||||
setUserLoading(true)
|
||||
try {
|
||||
if (!userQuery) {
|
||||
const res = await adminListUsers(0, 99, includeDeletedUsers)
|
||||
const raw = Array.isArray(res) ? res : (res.users || [])
|
||||
setUsers((raw || []).map(u => ({
|
||||
id: u.id,
|
||||
username: u.username || u.name || "",
|
||||
email: u.email || "",
|
||||
created: u.creationdate ? new Date(u.creationdate).toLocaleDateString() : "",
|
||||
deleted: !!u.deleted,
|
||||
raw: u
|
||||
})))
|
||||
setUserTotalCount(!Array.isArray(res) && res.pagination ? res.pagination.totalCount : (raw || []).length)
|
||||
} else {
|
||||
const res = await adminSearchUsers(userQuery, includeDeletedUsers, 100, 0)
|
||||
const raw = Array.isArray(res) ? res : (res.users || res)
|
||||
setUsers((raw || []).map(u => ({
|
||||
id: u.id,
|
||||
username: u.username || u.name || "",
|
||||
email: u.email || "",
|
||||
created: u.creationdate ? new Date(u.creationdate).toLocaleDateString() : "",
|
||||
deleted: !!u.deleted,
|
||||
raw: u
|
||||
})))
|
||||
setUserTotalCount((raw || []).length)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Admin: user search error", err)
|
||||
setUsers([])
|
||||
setUserTotalCount(0)
|
||||
} finally {
|
||||
setUserLoading(false)
|
||||
}
|
||||
}, 300)
|
||||
return () => clearTimeout(t)
|
||||
}, [userQuery, active, includeDeletedUsers])
|
||||
|
||||
// Filter logic
|
||||
let filteredDecks = allDecks.filter((deck) => {
|
||||
const typeMatch = selectedType === "All" || deck.type === selectedType
|
||||
const originMatch = selectedOrigin === "Mind" || deck.origin === selectedOrigin
|
||||
const searchMatch = !query || deck.name.toLowerCase().includes(query.toLowerCase())
|
||||
return typeMatch && originMatch && searchMatch
|
||||
})
|
||||
|
||||
// Sort logic
|
||||
filteredDecks = [...filteredDecks].sort((a, b) => {
|
||||
if (sortBy === "date-asc") {
|
||||
return a.created.localeCompare(b.created)
|
||||
} else if (sortBy === "date-desc") {
|
||||
return b.created.localeCompare(a.created)
|
||||
} else if (sortBy === "abc-asc") {
|
||||
return a.name.localeCompare(b.name)
|
||||
} else if (sortBy === "abc-desc") {
|
||||
return b.name.localeCompare(a.name)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
// Pagination logic
|
||||
const totalDecks = filteredDecks.length
|
||||
const totalPages = Math.max(1, Math.ceil(totalDecks / itemsPerPage))
|
||||
const startIndex = (currentPage - 1) * itemsPerPage
|
||||
const endIndex = startIndex + itemsPerPage
|
||||
const paginatedDecks = filteredDecks.slice(startIndex, endIndex)
|
||||
|
||||
// Reset to page 1 when filters change
|
||||
useEffect(() => {
|
||||
setCurrentPage(1)
|
||||
}, [selectedType, selectedOrigin, query, sortBy, itemsPerPage])
|
||||
|
||||
const NavButton = ({ id, icon: Icon, label }) => (
|
||||
<button
|
||||
onClick={() => setActive(id)}
|
||||
aria-pressed={active === id}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors text-sm font-medium
|
||||
${active === id ? "bg-emerald-500 text-white shadow-lg" : "text-gray-200 hover:bg-white/5"}`}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
// --- Teljes Deck handling (DeckManager-ről áthozva) ---
|
||||
const DeckHandling = () => (
|
||||
<motion.div
|
||||
key="decks"
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 8 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-semibold text-white">Deck management</h2>
|
||||
<ButtonGreen text="Create deck" onClick={goDeckCreator} width="w-36" />
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="flex gap-2 items-center w-full flex-wrap mb-4">
|
||||
<SearchBox
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
width={240}
|
||||
placeholder="Keresés..."
|
||||
className="mr-4"
|
||||
/>
|
||||
<FaFilter style={{ color: "var(--color-success)" }} className="mr-1 sm:mr-2 text-sm sm:text-base" />
|
||||
<span className="text-[color:var(--color-text)] font-semibold mr-1 sm:mr-2 text-xs sm:text-sm">Típus:</span>
|
||||
<button
|
||||
className={`px-2 sm:px-3 py-1 rounded-lg font-medium transition-all duration-200 text-xs sm:text-sm ${
|
||||
selectedType === "All"
|
||||
? "bg-[color:var(--color-surface-selected)] text-[color:var(--color-text)] border border-[color:var(--color-surface)]"
|
||||
: "text-[color:var(--color-text)] bg-[color:var(--color-success)]/10 hover:bg-[color:var(--color-success)]/30"
|
||||
}`}
|
||||
onClick={() => setSelectedType("All")}
|
||||
>
|
||||
Mind
|
||||
</button>
|
||||
{deckTypes.map((type) => (
|
||||
<button
|
||||
key={type.label}
|
||||
className={`px-2 sm:px-3 py-1 rounded-lg font-medium transition-all duration-200 ml-1 text-xs sm:text-sm ${
|
||||
selectedType === type.label
|
||||
? "text-[color:var(--color-text-inverse)] border border-[color:var(--color-surface)]"
|
||||
: "text-[color:var(--color-text)] bg-[color:var(--color-success)]/10 hover:bg-[color:var(--color-success)]/30"
|
||||
}`}
|
||||
style={selectedType === type.label ? { background: type.color } : undefined}
|
||||
onClick={() => setSelectedType(type.label)}
|
||||
>
|
||||
{type.label === "Luck"
|
||||
? "Szerencse"
|
||||
: type.label === "Question"
|
||||
? "Kérdés"
|
||||
: type.label === "Joker"
|
||||
? "Joker"
|
||||
: type.label}
|
||||
</button>
|
||||
))}
|
||||
<span className="text-[color:var(--color-text)] font-semibold mr-1 sm:mr-2 ml-1 sm:ml-2 text-xs sm:text-sm">Eredet:</span>
|
||||
<select
|
||||
className="px-2 sm:px-3 py-1 rounded-lg bg-[color:var(--color-success)]/10 hover:bg-[color:var(--color-success)]/30 text-[color:var(--color-text)] border-none focus:ring-2 focus:ring-[color:var(--color-success)] outline-none text-xs sm:text-sm"
|
||||
value={selectedOrigin}
|
||||
onChange={(e) => setSelectedOrigin(e.target.value)}
|
||||
>
|
||||
{origins.map((origin) => (
|
||||
<option key={origin} value={origin} className="bg-zinc-800 text-white">
|
||||
{origin}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<span className="text-[color:var(--color-text)] font-semibold mr-1 sm:mr-2 ml-1 sm:ml-2 flex items-center gap-1 text-xs sm:text-sm">
|
||||
Rendezés:
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 text-[color:var(--color-success)] hover:text-[color:var(--color-text)] focus:outline-none"
|
||||
onClick={() => setShowSortHelp(true)}
|
||||
aria-label="Rendezési magyarázat megnyitása"
|
||||
style={{ fontSize: 18, lineHeight: 1 }}
|
||||
>
|
||||
<FaQuestionCircle />
|
||||
</button>
|
||||
</span>
|
||||
<select
|
||||
className="px-2 sm:px-3 py-1 rounded-lg bg-[color:var(--color-success)]/10 hover:bg-[color:var(--color-success)]/30 text-[color:var(--color-text)] border-none focus:ring-2 focus:ring-[color:var(--color-success)] outline-none flex items-center text-xs sm:text-sm"
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
aria-label="Rendezés"
|
||||
>
|
||||
<option value="date-asc">📅↑</option>
|
||||
<option value="date-desc">📅↓</option>
|
||||
<option value="abc-asc">A→Z</option>
|
||||
<option value="abc-desc">Z→A</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Items per page selector and count */}
|
||||
<div className="flex flex-col md:flex-row gap-4 justify-between items-center mb-6 bg-[color:var(--color-surface)]/60 backdrop-blur-lg rounded-xl px-6 py-3 shadow">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-[color:var(--color-text-muted)] text-sm font-medium">
|
||||
Elemek oldalanként:
|
||||
</span>
|
||||
<select
|
||||
value={itemsPerPage}
|
||||
onChange={(e) => setItemsPerPage(Number(e.target.value))}
|
||||
className="px-3 py-1.5 rounded-lg bg-[color:var(--color-background)] text-[color:var(--color-text)] border border-[color:var(--color-surface-selected)] focus:ring-2 focus:ring-[color:var(--color-success)] outline-none transition-all duration-200"
|
||||
>
|
||||
<option value={20}>20</option>
|
||||
<option value={30}>30</option>
|
||||
<option value={40}>40</option>
|
||||
<option value={50}>50</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="text-[color:var(--color-text-muted)] text-sm">
|
||||
{totalDecks > 0 ? (
|
||||
<>
|
||||
{startIndex + 1}-{Math.min(endIndex, totalDecks)} / {totalDecks} pakli
|
||||
</>
|
||||
) : (
|
||||
<>0 pakli</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decks Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 sm:gap-6 lg:gap-8 mt-6 sm:mt-8">
|
||||
{/* Create New Deck card */}
|
||||
<div
|
||||
onClick={() => goDeckCreator()}
|
||||
className="flex flex-col items-center justify-center h-40 sm:h-48 bg-[color:var(--color-card)] border-2 border-dashed border-[color:var(--color-success)] rounded-xl sm:rounded-2xl cursor-pointer hover:bg-[color:var(--color-success)]/20 transition-all duration-200 shadow-lg"
|
||||
>
|
||||
<FaPlus style={{ color: "var(--color-success)" }} className="text-4xl sm:text-5xl mb-2" />
|
||||
<span className="text-[color:var(--color-text)] font-semibold text-sm sm:text-base">Új pakli létrehozása</span>
|
||||
</div>
|
||||
|
||||
{loading && <div className="col-span-full text-center text-[color:var(--color-text-muted)]">Betöltés...</div>}
|
||||
{!loading && paginatedDecks.length === 0 && <div className="col-span-full text-center text-[color:var(--color-text-muted)]">Nincsenek találatok.</div>}
|
||||
|
||||
{!loading && paginatedDecks.map((deck) => {
|
||||
const deckType = deckTypes.find((t) => t.label === deck.type)
|
||||
const borderColor = deckType ? deckType.color : "var(--color-success)"
|
||||
return (
|
||||
<div
|
||||
key={deck.id}
|
||||
className="flex flex-col justify-between h-40 sm:h-48 bg-[color:var(--color-card)] rounded-xl sm:rounded-2xl p-4 sm:p-6 shadow-lg border-t-4 hover:scale-105 transition-transform duration-200 cursor-pointer"
|
||||
style={{ borderTopColor: borderColor }}
|
||||
onClick={() => setSelectedDeck(deck)}
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
className="inline-block px-2 sm:px-3 py-1 rounded-full text-[10px] sm:text-xs font-bold mb-2"
|
||||
style={{
|
||||
background: deckType?.color,
|
||||
color: "var(--color-text-inverse)",
|
||||
}}
|
||||
>
|
||||
{deck.type === "Luck"
|
||||
? "Szerencse"
|
||||
: deck.type === "Question"
|
||||
? "Kérdés"
|
||||
: "Joker"}
|
||||
</span>
|
||||
<h2 className="text-xl font-bold text-[color:var(--color-text)] mb-1 truncate">
|
||||
{deck.name}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="text-[color:var(--color-text-muted)] text-sm mt-2">
|
||||
Létrehozva: {deck.created}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Pagination Controls */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center items-center gap-2 mt-8">
|
||||
<button
|
||||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2 ${
|
||||
currentPage === 1
|
||||
? 'bg-[color:var(--color-surface)] text-[color:var(--color-text-muted)] cursor-not-allowed'
|
||||
: 'bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] hover:bg-[color:var(--color-success)]/80 hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
<FaChevronLeft />
|
||||
Előző
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{[...Array(totalPages)].map((_, index) => {
|
||||
const pageNum = index + 1
|
||||
if (
|
||||
pageNum === 1 ||
|
||||
pageNum === totalPages ||
|
||||
(pageNum >= currentPage - 1 && pageNum <= currentPage + 1)
|
||||
) {
|
||||
return (
|
||||
<button
|
||||
key={pageNum}
|
||||
onClick={() => setCurrentPage(pageNum)}
|
||||
className={`w-10 h-10 rounded-lg font-medium transition-all duration-200 ${
|
||||
currentPage === pageNum
|
||||
? 'bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] scale-110 shadow-lg'
|
||||
: 'bg-[color:var(--color-surface)] text-[color:var(--color-text)] hover:bg-[color:var(--color-surface-selected)]'
|
||||
}`}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
)
|
||||
} else if (pageNum === currentPage - 2 || pageNum === currentPage + 2) {
|
||||
return <span key={pageNum} className="text-[color:var(--color-text-muted)]">...</span>
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2 ${
|
||||
currentPage === totalPages
|
||||
? 'bg-[color:var(--color-surface)] text-[color:var(--color-text-muted)] cursor-not-allowed'
|
||||
: 'bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] hover:bg-[color:var(--color-success)]/80 hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
Következő
|
||||
<FaChevronRight />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sort help popup */}
|
||||
{showSortHelp && (
|
||||
<PopUp onClose={() => setShowSortHelp(false)}>
|
||||
<h2 className="text-lg font-bold mb-4">Rendezési lehetőségek magyarázata</h2>
|
||||
<ul className="space-y-2 text-[color:var(--color-night)]">
|
||||
<li><span className="font-bold">📅↑</span> – Dátum szerint növekvő sorrendben (legrégebbi elöl)</li>
|
||||
<li><span className="font-bold">📅↓</span> – Dátum szerint csökkenő sorrendban (legújabb elöl)</li>
|
||||
<li><span className="font-bold">A→Z</span> – Név szerint növekvő sorrendben (A-tól Z-ig)</li>
|
||||
<li><span className="font-bold">Z→A</span> – Név szerint csökkenő sorrendben (Z-től A-ig)</li>
|
||||
</ul>
|
||||
<div className="mt-6 flex justify-end">
|
||||
<button className="px-4 py-2 rounded-lg bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] font-semibold" onClick={() => setShowSortHelp(false)}>Bezárás</button>
|
||||
</div>
|
||||
</PopUp>
|
||||
)}
|
||||
|
||||
{/* Deck Info Popup */}
|
||||
{selectedDeck && <DeckInfoPopUp deck={selectedDeck} onClose={() => setSelectedDeck(null)} />}
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
|
||||
const UserHandlingMock = () => (
|
||||
<motion.div
|
||||
key="users"
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 8 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-semibold text-white">User management</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<ButtonGreen text="Invite user" onClick={() => alert("Invite / invite-flow to implement")} width="w-36" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="flex gap-2 items-center w-full flex-wrap mb-4">
|
||||
<SearchBox
|
||||
value={userQuery}
|
||||
onChange={(e) => setUserQuery(e.target.value)}
|
||||
width={240}
|
||||
placeholder="Keresés felhasználók között..."
|
||||
className="mr-4"
|
||||
/>
|
||||
<label className="text-[color:var(--color-text)] font-semibold text-xs sm:text-sm flex items-center gap-2">
|
||||
<input type="checkbox" checked={includeDeletedUsers} onChange={e => setIncludeDeletedUsers(e.target.checked)} className="mr-1" />
|
||||
Törölt is
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="bg-[color:var(--color-surface)]/60 backdrop-blur-lg rounded-xl px-6 py-3 shadow mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-[color:var(--color-text-muted)] text-sm font-medium">Elemek oldalanként:</span>
|
||||
<select
|
||||
value={userItemsPerPage}
|
||||
onChange={(e) => setUserItemsPerPage(Number(e.target.value))}
|
||||
className="px-3 py-1.5 rounded-lg bg-[color:var(--color-background)] text-[color:var(--color-text)] border border-[color:var(--color-surface-selected)] focus:ring-2 focus:ring-[color:var(--color-success)] outline-none transition-all duration-200"
|
||||
>
|
||||
<option value={10}>10</option>
|
||||
<option value={20}>20</option>
|
||||
<option value={30}>30</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="text-[color:var(--color-text-muted)] text-sm">
|
||||
{userTotalCount > 0 ? `${Math.min((userCurrentPage-1)*userItemsPerPage + 1, userTotalCount)}-${Math.min(userCurrentPage*userItemsPerPage, userTotalCount)} / ${userTotalCount} felhasználó` : '0 felhasználó'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User list */}
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{userLoading && <div className="text-center text-[color:var(--color-text-muted)]">Betöltés...</div>}
|
||||
{!userLoading && users.length === 0 && <div className="text-center text-[color:var(--color-text-muted)]">Nincsenek találatok.</div>}
|
||||
|
||||
{!userLoading && users.slice((userCurrentPage-1)*userItemsPerPage, userCurrentPage*userItemsPerPage).map(u => (
|
||||
<div key={u.id} className="flex items-center justify-between bg-[color:var(--color-card)] rounded-xl p-4 shadow">
|
||||
<div>
|
||||
<div className="font-semibold text-[color:var(--color-text)]">{u.username}</div>
|
||||
<div className="text-sm text-[color:var(--color-text-muted)]">{u.email}</div>
|
||||
<div className="text-xs text-[color:var(--color-text-muted)] mt-1">Létrehozva: {u.created}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-2 py-1 rounded text-xs ${u.deleted ? 'bg-red-600 text-white' : 'bg-green-600 text-white'}`}>{u.deleted ? 'Törölt' : 'Aktív'}</span>
|
||||
<button className="px-3 py-1 rounded bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)]" onClick={() => setSelectedUser(u)}>Megtekint</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pagination controls for users */}
|
||||
{ Math.ceil(Math.max(userTotalCount, users.length) / userItemsPerPage) > 1 && (
|
||||
<div className="flex justify-center items-center gap-2 mt-6">
|
||||
{(() => {
|
||||
const totalPages = Math.max(1, Math.ceil(Math.max(userTotalCount, users.length) / userItemsPerPage))
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setUserCurrentPage(p => Math.max(1, p-1))} className="px-4 py-2 rounded-lg bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)]">Előző</button>
|
||||
<div className="flex items-center gap-2">
|
||||
{[...Array(totalPages)].map((_, i) => {
|
||||
const p = i+1
|
||||
if (p === 1 || p === totalPages || (p >= userCurrentPage-1 && p <= userCurrentPage+1)) {
|
||||
return <button key={p} onClick={() => setUserCurrentPage(p)} className={`w-10 h-10 rounded-lg ${userCurrentPage===p ? 'bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)]' : 'bg-[color:var(--color-surface)] text-[color:var(--color-text)]'}`}>{p}</button>
|
||||
}
|
||||
if (p === userCurrentPage-2 || p === userCurrentPage+2) return <span key={p} className="text-[color:var(--color-text-muted)]">...</span>
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
<button onClick={() => setUserCurrentPage(p => Math.min(totalPages, p+1))} className="px-4 py-2 rounded-lg bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)]">Következő</button>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* User details popup */}
|
||||
{selectedUser && (
|
||||
<PopUp onClose={() => setSelectedUser(null)}>
|
||||
<h2 className="text-lg font-bold mb-2">{selectedUser.username}</h2>
|
||||
<div className="text-sm text-[color:var(--color-text-muted)] mb-4">
|
||||
Email: {selectedUser.email} <br />
|
||||
Létrehozva: {selectedUser.created} <br />
|
||||
Status: {selectedUser.deleted ? 'Törölt' : 'Aktív'}
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
{!selectedUser.deleted && (
|
||||
<button
|
||||
className="px-3 py-2 rounded bg-yellow-600 text-white"
|
||||
onClick={async () => {
|
||||
if (!confirm('Biztosan deaktiválod ezt a felhasználót?')) return
|
||||
try {
|
||||
const uuid = await resolveUserUuid(selectedUser)
|
||||
if (!uuid) {
|
||||
alert("Nem található érvényes userId (UUID) a kiválasztott felhasználónál. Ellenőrizd a backend választ vagy a user objektumot a konzolban.")
|
||||
console.error("Selected user object:", selectedUser)
|
||||
return
|
||||
}
|
||||
await adminDeactivateUser(uuid)
|
||||
// reflect change locally
|
||||
setUsers(curr => curr.map(u => u.id === selectedUser.id ? { ...u, deleted: true } : u))
|
||||
setSelectedUser(prev => ({ ...prev, deleted: true }))
|
||||
alert('Felhasználó deaktiválva.')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert(getApiErrorMessage(err) || 'Hiba a deaktiválás során.')
|
||||
}
|
||||
}}
|
||||
>
|
||||
Deaktiválás
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="px-3 py-2 rounded bg-red-600 text-white"
|
||||
onClick={async () => {
|
||||
if (!confirm('Végleg törlöd a felhasználót?')) return
|
||||
try {
|
||||
const uuid = await resolveUserUuid(selectedUser)
|
||||
if (!uuid) {
|
||||
alert("Nem található érvényes userId (UUID) a kiválasztott felhasználónál. Ellenőrizd a backend választ vagy a user objektumot a konzolban.")
|
||||
console.error("Selected user object:", selectedUser)
|
||||
return
|
||||
}
|
||||
await adminDeleteUser(uuid)
|
||||
setUsers(curr => curr.filter(u => u.id !== selectedUser.id))
|
||||
setSelectedUser(null)
|
||||
alert('Felhasználó törölve.')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert(getApiErrorMessage(err) || 'Hiba a törlés során.')
|
||||
}
|
||||
}}
|
||||
>
|
||||
Törlés
|
||||
</button>
|
||||
<button className="px-3 py-2 rounded bg-[color:var(--color-surface)]" onClick={() => setSelectedUser(null)}>Bezár</button>
|
||||
</div>
|
||||
</PopUp>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
|
||||
// ---------- NEW: UUID helper + resolver ----------
|
||||
const isUuid = (val) => {
|
||||
if (!val || typeof val !== "string") return false
|
||||
// simple RFC4122 v4-ish UUID check (hex + dashes)
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(val.trim())
|
||||
}
|
||||
|
||||
// Try various places on the user object for a UUID, fallback to search by username/email
|
||||
const resolveUserUuid = async (user) => {
|
||||
if (!user) return null
|
||||
const candidates = [
|
||||
user.id,
|
||||
user.raw?.id,
|
||||
user.raw?.userid,
|
||||
user.raw?.userId,
|
||||
user.raw?.uuid,
|
||||
user.raw?.user_id
|
||||
]
|
||||
for (const c of candidates) if (isUuid(String(c || ""))) return String(c).trim()
|
||||
|
||||
// fallback: try to find canonical user by username or email via adminSearchUsers
|
||||
const q = user.username || user.email || user.raw?.username || user.raw?.email
|
||||
if (!q) return null
|
||||
try {
|
||||
const res = await adminSearchUsers(q, includeDeletedUsers ? true : false, 10, 0)
|
||||
const arr = Array.isArray(res) ? res : (res.users || res || [])
|
||||
if (Array.isArray(arr) && arr.length) {
|
||||
// try to match by username/email exactly, or take first with valid id
|
||||
const exact = arr.find(u => (u.username && u.username === user.username) || (u.email && u.email === user.email))
|
||||
const pick = exact || arr[0]
|
||||
if (pick && isUuid(pick.id)) return pick.id
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("resolveUserUuid: search failed", err)
|
||||
}
|
||||
return null
|
||||
}
|
||||
// ---------- end new helpers ----------
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen flex flex-col relative overflow-x-hidden">
|
||||
<div className="fixed inset-0 -z-10 pointer-events-none">
|
||||
<Background />
|
||||
</div>
|
||||
|
||||
<div className="fixed top-0 left-0 right-0 z-30">
|
||||
<Navbar />
|
||||
</div>
|
||||
|
||||
<main className="flex-1 min-h-[calc(100vh-64px)] mt-[64px] px-4 sm:px-6 py-8">
|
||||
<div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-12 gap-6">
|
||||
{/* Left navigation column */}
|
||||
<aside className="md:col-span-3 lg:col-span-2 bg-white/5 rounded-2xl p-4 sticky top-[88px] h-fit">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Admin panel</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<NavButton id="decks" icon={FaLayerGroup} label="Deck handling" />
|
||||
<NavButton id="users" icon={FaUser} label="User handling" />
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Right content column */}
|
||||
<section className="md:col-span-9 lg:col-span-10">
|
||||
<div className="bg-white/6 rounded-2xl p-6 min-h-[300px]">
|
||||
{active === "decks" ? <DeckHandling /> : <UserHandlingMock />}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -36,8 +36,14 @@ export const ROUTES = {
|
||||
REPORTS: '/report',
|
||||
CONTACTS: '/contacts',
|
||||
TEST: '/test',
|
||||
|
||||
// admin
|
||||
ADMIN: '/admin',
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Helper functions to generate dynamic routes
|
||||
export const routeHelpers = {
|
||||
deckDetails: (deckId) => `/deck/${deckId}`,
|
||||
|
||||
Reference in New Issue
Block a user