This commit is contained in:
2026-04-19 13:22:48 +03:00
parent 1c1d87176e
commit 538612c8ea
14 changed files with 661 additions and 42 deletions
+46 -10
View File
@@ -24,8 +24,10 @@ import {
} from "@/components/motion";
import { useAdminAnalytics, useAdminUsers } from "@/lib/api/admin/use-hooks";
import type { AdminUserDto, AnalyticsOverviewDto } from "@/lib/api/admin/types";
import { formatRoleLabel, isAdminRole } from "@/lib/auth/roles";
import { LuUsers, LuChartBar, LuActivity, LuShield } from "react-icons/lu";
import { useState } from "react";
import { useSession } from "next-auth/react";
type AdminTab = "overview" | "users";
@@ -81,16 +83,21 @@ export default function AdminContent() {
const t = useTranslations("admin");
const tCommon = useTranslations("common");
const [activeTab, setActiveTab] = useState<AdminTab>("overview");
const { data: session, status } = useSession();
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.100", "gray.700");
const canAccessAdmin = isAdminRole(session?.user?.roles);
const { data: analyticsData, isLoading: analyticsLoading } =
useAdminAnalytics();
const { data: usersData, isLoading: usersLoading } = useAdminUsers();
useAdminAnalytics(canAccessAdmin);
const { data: usersData, isLoading: usersLoading } = useAdminUsers(
undefined,
canAccessAdmin,
);
const analytics = analyticsData?.data as AnalyticsOverviewDto | undefined;
const users = (usersData?.data as AdminUserDto[] | undefined) ?? [];
const users = usersData?.data?.items ?? [];
const tabs: { key: AdminTab; label: string }[] = [
{ key: "overview", label: t("overview") },
@@ -104,6 +111,37 @@ export default function AdminContent() {
return user.email.split("@")[0];
};
if (status === "loading") {
return (
<Flex justify="center" py={16}>
<Spinner size="lg" color="primary.500" />
</Flex>
);
}
if (!canAccessAdmin) {
return (
<SlideUp>
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
<Card.Body py={10}>
<VStack gap={3}>
<Badge colorPalette="red" variant="subtle" borderRadius="full">
<LuShield />
Restricted
</Badge>
<Heading as="h2" size="md">
Admin access required
</Heading>
<Text color="fg.muted" textAlign="center" maxW="md">
This area is only available to superadmin accounts.
</Text>
</VStack>
</Card.Body>
</Card.Root>
</SlideUp>
);
}
return (
<SlideUp>
<Box>
@@ -156,7 +194,7 @@ export default function AdminContent() {
<StaggerItem>
<AdminStat
label={t("total-users")}
value={analytics?.totalUsers ?? 0}
value={analytics?.totalUsers ?? analytics?.users?.total ?? 0}
icon={<LuUsers />}
colorPalette="primary"
/>
@@ -164,7 +202,7 @@ export default function AdminContent() {
<StaggerItem>
<AdminStat
label={t("total-predictions")}
value={analytics?.totalPredictions ?? 0}
value={analytics?.totalPredictions ?? analytics?.predictions ?? 0}
icon={<LuChartBar />}
colorPalette="green"
/>
@@ -172,7 +210,7 @@ export default function AdminContent() {
<StaggerItem>
<AdminStat
label={t("active-users")}
value={analytics?.activeUsers ?? 0}
value={analytics?.activeUsers ?? analytics?.users?.active ?? 0}
icon={<LuActivity />}
colorPalette="orange"
/>
@@ -244,14 +282,12 @@ export default function AdminContent() {
</Text>
<Flex flex={1} justify="center">
<Badge
colorPalette={
user.role === "ADMIN" ? "red" : "gray"
}
colorPalette={isAdminRole([user.role]) ? "red" : "gray"}
variant="subtle"
fontSize="2xs"
borderRadius="full"
>
{user.role || "User"}
{formatRoleLabel(user.role)}
</Badge>
</Flex>
<Flex flex={1} justify="center">
+7 -7
View File
@@ -39,6 +39,7 @@ import { Skeleton } from "@/components/ui/feedback/skeleton";
import { signOut, useSession } from "next-auth/react";
import { authConfig } from "@/config/auth";
import { LoginModal } from "@/components/auth/login-modal";
import { isAdminRole } from "@/lib/auth/roles";
import { LuLogIn, LuUser, LuShield, LuZap } from "react-icons/lu";
import GlobalSearch from "@/components/search/global-search";
@@ -81,13 +82,12 @@ export default function Header() {
<LuUser />
{t("nav.profile")}
</MenuItem>
{session?.user &&
session.user.roles?.includes("ADMIN") && (
<MenuItem value="admin" onClick={() => router.push("/admin")}>
<LuShield />
{t("nav.admin")}
</MenuItem>
)}
{session?.user && isAdminRole(session.user.roles) && (
<MenuItem value="admin" onClick={() => router.push("/admin")}>
<LuShield />
{t("nav.admin")}
</MenuItem>
)}
<MenuItem onClick={handleLogout} value="sign-out">
{t("auth.sign-out")}
</MenuItem>
+14 -9
View File
@@ -25,12 +25,17 @@ export default function GlobalSearch() {
const containerRef = useRef<HTMLDivElement>(null);
const router = useRouter();
const isApplePlatform =
typeof navigator !== "undefined" &&
/Mac|iPhone|iPad|iPod/i.test(navigator.platform);
const shortcutLabel = isApplePlatform ? "Cmd+K" : "Ctrl+K";
const shortcutCapsule = isApplePlatform ? "⌘K" : "Ctrl+K";
const bg = useColorModeValue("white", "gray.900");
const borderColor = useColorModeValue("gray.200", "gray.700");
const hoverBg = useColorModeValue("gray.50", "gray.800");
const inputBg = useColorModeValue("gray.50", "gray.800");
// Debounce search input
useEffect(() => {
const timer = setTimeout(() => setDebouncedQuery(query), 300);
return () => clearTimeout(timer);
@@ -42,7 +47,6 @@ export default function GlobalSearch() {
const teams: TeamDto[] = searchData?.data ?? [];
// Close dropdown on outside click
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (
@@ -56,7 +60,6 @@ export default function GlobalSearch() {
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
// Keyboard shortcut: Ctrl+K to focus
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
@@ -83,8 +86,11 @@ export default function GlobalSearch() {
);
return (
<Box ref={containerRef} position="relative" w={{ base: "full", lg: "280px" }}>
{/* Search Input */}
<Box
ref={containerRef}
position="relative"
w={{ base: "full", lg: "280px" }}
>
<Flex
align="center"
bg={inputBg}
@@ -110,7 +116,7 @@ export default function GlobalSearch() {
setIsOpen(true);
}}
onFocus={() => query.length >= 2 && setIsOpen(true)}
placeholder="Takım ara... (Ctrl+K)"
placeholder={`Takim ara... (${shortcutLabel})`}
variant="flushed"
size="sm"
px={2}
@@ -142,11 +148,10 @@ export default function GlobalSearch() {
borderRadius="md"
fontFamily="mono"
>
K
{shortcutCapsule}
</Text>
</Flex>
{/* Dropdown Results */}
{isOpen && debouncedQuery.length >= 2 && (
<Box
position="absolute"
@@ -170,7 +175,7 @@ export default function GlobalSearch() {
) : teams.length === 0 ? (
<Flex justify="center" py={6}>
<Text fontSize="sm" color="fg.muted">
Sonuç bulunamadı
Sonuc bulunamadi
</Text>
</Flex>
) : (