This commit is contained in:
@@ -15,7 +15,10 @@ import {
|
||||
Separator,
|
||||
Input,
|
||||
} from "@chakra-ui/react";
|
||||
import { NativeSelectRoot, NativeSelectField } from "@/components/ui/forms/native-select";
|
||||
import {
|
||||
NativeSelectRoot,
|
||||
NativeSelectField,
|
||||
} from "@/components/ui/forms/native-select";
|
||||
import { useTranslations, useFormatter } from "next-intl";
|
||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||
import {
|
||||
@@ -27,7 +30,13 @@ import {
|
||||
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, LuPencil } from "react-icons/lu";
|
||||
import {
|
||||
LuUsers,
|
||||
LuChartBar,
|
||||
LuActivity,
|
||||
LuShield,
|
||||
LuPencil,
|
||||
} from "react-icons/lu";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { EditUserModal } from "./edit-user-modal";
|
||||
@@ -88,13 +97,19 @@ export default function AdminContent() {
|
||||
const format = useFormatter();
|
||||
const [activeTab, setActiveTab] = useState<AdminTab>("overview");
|
||||
const [editingUser, setEditingUser] = useState<AdminUserDto | null>(null);
|
||||
const [searchParams, setSearchParams] = useState({ search: "", role: "", subscriptionStatus: "", page: 1, limit: 10 });
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
search: "",
|
||||
role: "",
|
||||
subscriptionStatus: "",
|
||||
page: 1,
|
||||
limit: 10,
|
||||
});
|
||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedSearch(searchParams.search);
|
||||
setSearchParams(prev => ({ ...prev, page: 1 }));
|
||||
setSearchParams((prev) => ({ ...prev, page: 1 }));
|
||||
}, 500);
|
||||
return () => clearTimeout(handler);
|
||||
}, [searchParams.search]);
|
||||
@@ -113,7 +128,7 @@ export default function AdminContent() {
|
||||
role: searchParams.role,
|
||||
subscriptionStatus: searchParams.subscriptionStatus,
|
||||
page: searchParams.page,
|
||||
limit: searchParams.limit
|
||||
limit: searchParams.limit,
|
||||
},
|
||||
canAccessAdmin,
|
||||
);
|
||||
@@ -150,13 +165,13 @@ export default function AdminContent() {
|
||||
<VStack gap={3}>
|
||||
<Badge colorPalette="red" variant="subtle" borderRadius="full">
|
||||
<LuShield />
|
||||
Restricted
|
||||
{t("restricted")}
|
||||
</Badge>
|
||||
<Heading as="h2" size="md">
|
||||
Admin access required
|
||||
{t("admin-access-required")}
|
||||
</Heading>
|
||||
<Text color="fg.muted" textAlign="center" maxW="md">
|
||||
This area is only available to superadmin accounts.
|
||||
{t("admin-access-description")}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Card.Body>
|
||||
@@ -236,7 +251,7 @@ export default function AdminContent() {
|
||||
</StaggerItem>
|
||||
<StaggerItem>
|
||||
<AdminStat
|
||||
label={t("premium-users", { fallback: "Premium Users" })}
|
||||
label={t("premium-users")}
|
||||
value={analytics?.users?.premium ?? 0}
|
||||
icon={<LuShield />}
|
||||
colorPalette="purple"
|
||||
@@ -272,32 +287,49 @@ export default function AdminContent() {
|
||||
<Card.Body py={4}>
|
||||
<SimpleGrid columns={{ base: 1, md: 3 }} gap={4}>
|
||||
<Input
|
||||
placeholder="E-posta veya isim ara..."
|
||||
placeholder={t("search-users-placeholder")}
|
||||
value={searchParams.search}
|
||||
onChange={(e) => setSearchParams({ ...searchParams, search: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
search: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<NativeSelectRoot>
|
||||
<NativeSelectField
|
||||
placeholder="Tüm Rolleri Gör"
|
||||
placeholder={t("all-roles")}
|
||||
value={searchParams.role}
|
||||
onChange={(e) => setSearchParams({ ...searchParams, role: e.target.value, page: 1 })}
|
||||
onChange={(e) =>
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
role: e.target.value,
|
||||
page: 1,
|
||||
})
|
||||
}
|
||||
items={[
|
||||
{ label: "Standart Kullanıcı", value: "user" },
|
||||
{ label: "Admin", value: "superadmin" }
|
||||
{ label: t("standard-user"), value: "user" },
|
||||
{ label: t("superadmin"), value: "superadmin" },
|
||||
]}
|
||||
/>
|
||||
</NativeSelectRoot>
|
||||
<NativeSelectRoot>
|
||||
<NativeSelectField
|
||||
placeholder="Tüm Paketleri Gör"
|
||||
placeholder={t("all-plans")}
|
||||
value={searchParams.subscriptionStatus}
|
||||
onChange={(e) => setSearchParams({ ...searchParams, subscriptionStatus: e.target.value, page: 1 })}
|
||||
onChange={(e) =>
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
subscriptionStatus: e.target.value,
|
||||
page: 1,
|
||||
})
|
||||
}
|
||||
items={[
|
||||
{ label: "Ücretsiz (Free)", value: "free" },
|
||||
{ label: t("plan-free"), value: "free" },
|
||||
{ label: "Plus", value: "plus" },
|
||||
{ label: "Premium", value: "premium" },
|
||||
{ label: "Gecikmiş", value: "past_due" },
|
||||
{ label: "İptal", value: "cancelled" }
|
||||
{ label: t("plan-past-due"), value: "past_due" },
|
||||
{ label: t("plan-cancelled"), value: "cancelled" },
|
||||
]}
|
||||
/>
|
||||
</NativeSelectRoot>
|
||||
@@ -310,7 +342,11 @@ export default function AdminContent() {
|
||||
<Spinner size="lg" color="primary.500" />
|
||||
</Flex>
|
||||
) : users.length > 0 ? (
|
||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
|
||||
<Card.Root
|
||||
bg={cardBg}
|
||||
borderColor={borderColor}
|
||||
borderRadius="xl"
|
||||
>
|
||||
<Card.Body>
|
||||
<VStack gap={0} align="stretch">
|
||||
{/* Table Header */}
|
||||
@@ -330,7 +366,7 @@ export default function AdminContent() {
|
||||
{t("user-role")}
|
||||
</Text>
|
||||
<Text flex={1} textAlign="center">
|
||||
{t("subscription", { fallback: "Subscription" })}
|
||||
{t("subscription")}
|
||||
</Text>
|
||||
<Text flex={1} textAlign="center">
|
||||
{t("user-status")}
|
||||
@@ -357,7 +393,12 @@ export default function AdminContent() {
|
||||
>
|
||||
{getUserDisplayName(user)}
|
||||
</Text>
|
||||
<Text flex={2} fontSize="sm" color="fg.muted" truncate>
|
||||
<Text
|
||||
flex={2}
|
||||
fontSize="sm"
|
||||
color="fg.muted"
|
||||
truncate
|
||||
>
|
||||
{user.email}
|
||||
</Text>
|
||||
<Flex flex={1} justify="center">
|
||||
@@ -372,9 +413,20 @@ export default function AdminContent() {
|
||||
{formatRoleLabel(user.role)}
|
||||
</Badge>
|
||||
</Flex>
|
||||
<Flex flex={1} justify="center" direction="column" align="center" gap={1}>
|
||||
<Flex
|
||||
flex={1}
|
||||
justify="center"
|
||||
direction="column"
|
||||
align="center"
|
||||
gap={1}
|
||||
>
|
||||
<Badge
|
||||
colorPalette={user.subscriptionStatus === "premium" || user.subscriptionStatus === "plus" ? "purple" : "gray"}
|
||||
colorPalette={
|
||||
user.subscriptionStatus === "premium" ||
|
||||
user.subscriptionStatus === "plus"
|
||||
? "purple"
|
||||
: "gray"
|
||||
}
|
||||
variant="subtle"
|
||||
fontSize="2xs"
|
||||
borderRadius="full"
|
||||
@@ -384,7 +436,14 @@ export default function AdminContent() {
|
||||
</Badge>
|
||||
{user.subscriptionExpiresAt ? (
|
||||
<Text fontSize="2xs" color="fg.muted">
|
||||
{format.dateTime(new Date(user.subscriptionExpiresAt), { year: 'numeric', month: '2-digit', day: '2-digit' })}
|
||||
{format.dateTime(
|
||||
new Date(user.subscriptionExpiresAt),
|
||||
{
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
},
|
||||
)}
|
||||
</Text>
|
||||
) : (
|
||||
<Text fontSize="2xs" color="fg.muted">
|
||||
@@ -436,25 +495,45 @@ export default function AdminContent() {
|
||||
|
||||
{/* Pagination */}
|
||||
{meta && meta.totalPages > 1 && (
|
||||
<Flex justify="center" pt={4} pb={2} gap={2} borderTopWidth="1px" borderColor={borderColor} mt={2}>
|
||||
<Flex
|
||||
justify="center"
|
||||
pt={4}
|
||||
pb={2}
|
||||
gap={2}
|
||||
borderTopWidth="1px"
|
||||
borderColor={borderColor}
|
||||
mt={2}
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={!meta.hasPreviousPage}
|
||||
onClick={() => setSearchParams({ ...searchParams, page: meta.page - 1 })}
|
||||
onClick={() =>
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
page: meta.page - 1,
|
||||
})
|
||||
}
|
||||
>
|
||||
Önceki
|
||||
{tCommon("previous")}
|
||||
</Button>
|
||||
<Flex align="center" gap={2} fontSize="sm">
|
||||
<Text>Sayfa {meta.page} / {meta.totalPages}</Text>
|
||||
<Text>
|
||||
{tCommon("page")} {meta.page} / {meta.totalPages}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={!meta.hasNextPage}
|
||||
onClick={() => setSearchParams({ ...searchParams, page: meta.page + 1 })}
|
||||
onClick={() =>
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
page: meta.page + 1,
|
||||
})
|
||||
}
|
||||
>
|
||||
Sonraki
|
||||
{tCommon("next")}
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user