main
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 2m23s

This commit is contained in:
2026-05-12 17:41:16 +03:00
parent b2ccc98226
commit 66877b88ca
7 changed files with 666 additions and 242 deletions
+111 -32
View File
@@ -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>
)}