gg
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 2m41s

This commit is contained in:
2026-05-12 03:03:49 +03:00
parent e6e58b4433
commit b2ccc98226
24 changed files with 814 additions and 164 deletions
+233 -108
View File
@@ -13,8 +13,10 @@ import {
Spinner,
Button,
Separator,
Input,
} from "@chakra-ui/react";
import { useTranslations } from "next-intl";
import { NativeSelectRoot, NativeSelectField } from "@/components/ui/forms/native-select";
import { useTranslations, useFormatter } from "next-intl";
import { useColorModeValue } from "@/components/ui/color-mode";
import {
SlideUp,
@@ -25,9 +27,10 @@ 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 } from "react-icons/lu";
import { useState } from "react";
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";
type AdminTab = "overview" | "users";
@@ -82,7 +85,20 @@ function AdminStat({ label, value, icon, colorPalette }: AdminStatProps) {
export default function AdminContent() {
const t = useTranslations("admin");
const tCommon = useTranslations("common");
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 [debouncedSearch, setDebouncedSearch] = useState("");
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedSearch(searchParams.search);
setSearchParams(prev => ({ ...prev, page: 1 }));
}, 500);
return () => clearTimeout(handler);
}, [searchParams.search]);
const { data: session, status } = useSession();
const cardBg = useColorModeValue("white", "gray.800");
@@ -92,12 +108,19 @@ export default function AdminContent() {
const { data: analyticsData, isLoading: analyticsLoading } =
useAdminAnalytics(canAccessAdmin);
const { data: usersData, isLoading: usersLoading } = useAdminUsers(
undefined,
{
search: debouncedSearch,
role: searchParams.role,
subscriptionStatus: searchParams.subscriptionStatus,
page: searchParams.page,
limit: searchParams.limit
},
canAccessAdmin,
);
const analytics = analyticsData?.data as AnalyticsOverviewDto | undefined;
const users = usersData?.data?.items ?? [];
const meta = usersData?.data?.meta;
const tabs: { key: AdminTab; label: string }[] = [
{ key: "overview", label: t("overview") },
@@ -242,113 +265,215 @@ export default function AdminContent() {
))}
{/* Users Tab */}
{activeTab === "users" &&
(usersLoading ? (
<Flex justify="center" py={16}>
<Spinner size="lg" color="primary.500" />
</Flex>
) : users.length > 0 ? (
{activeTab === "users" && (
<VStack gap={4} align="stretch">
{/* Filters */}
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
<Card.Body>
<VStack gap={0} align="stretch">
{/* Table Header */}
<Flex
px={4}
py={2}
bg="bg.muted"
borderRadius="lg"
mb={2}
fontWeight="semibold"
fontSize="xs"
color="fg.muted"
>
<Text flex={2}>{t("user-name")}</Text>
<Text flex={2}>{t("user-email")}</Text>
<Text flex={1} textAlign="center">
{t("user-role")}
</Text>
<Text flex={1} textAlign="center">
{t("subscription", { fallback: "Subscription" })}
</Text>
<Text flex={1} textAlign="center">
{t("user-status")}
</Text>
</Flex>
{/* User Rows */}
{users.map((user: AdminUserDto, idx: number) => (
<Box key={user.id ?? idx}>
{idx > 0 && <Separator />}
<Flex
px={4}
py={3}
align="center"
_hover={{ bg: "bg.muted" }}
borderRadius="lg"
>
<Text
flex={2}
fontSize="sm"
fontWeight="medium"
truncate
>
{getUserDisplayName(user)}
</Text>
<Text flex={2} fontSize="sm" color="fg.muted" truncate>
{user.email}
</Text>
<Flex flex={1} justify="center">
<Badge
colorPalette={
isAdminRole([user.role]) ? "red" : "gray"
}
variant="subtle"
fontSize="2xs"
borderRadius="full"
>
{formatRoleLabel(user.role)}
</Badge>
</Flex>
<Flex flex={1} justify="center">
<Badge
colorPalette={
user.subscriptionStatus === "premium"
? "purple"
: user.subscriptionStatus === "plus"
? "blue"
: "gray"
}
variant="subtle"
fontSize="2xs"
borderRadius="full"
textTransform="capitalize"
>
{user.subscriptionStatus || "free"}
</Badge>
</Flex>
<Flex flex={1} justify="center">
<Badge
colorPalette={user.isActive ? "green" : "gray"}
variant="subtle"
fontSize="2xs"
borderRadius="full"
>
{user.isActive
? tCommon("active")
: tCommon("inactive")}
</Badge>
</Flex>
</Flex>
</Box>
))}
</VStack>
<Card.Body py={4}>
<SimpleGrid columns={{ base: 1, md: 3 }} gap={4}>
<Input
placeholder="E-posta veya isim ara..."
value={searchParams.search}
onChange={(e) => setSearchParams({ ...searchParams, search: e.target.value })}
/>
<NativeSelectRoot>
<NativeSelectField
placeholder="Tüm Rolleri Gör"
value={searchParams.role}
onChange={(e) => setSearchParams({ ...searchParams, role: e.target.value, page: 1 })}
items={[
{ label: "Standart Kullanıcı", value: "user" },
{ label: "Admin", value: "superadmin" }
]}
/>
</NativeSelectRoot>
<NativeSelectRoot>
<NativeSelectField
placeholder="Tüm Paketleri Gör"
value={searchParams.subscriptionStatus}
onChange={(e) => setSearchParams({ ...searchParams, subscriptionStatus: e.target.value, page: 1 })}
items={[
{ label: "Ücretsiz (Free)", value: "free" },
{ label: "Plus", value: "plus" },
{ label: "Premium", value: "premium" },
{ label: "Gecikmiş", value: "past_due" },
{ label: "İptal", value: "cancelled" }
]}
/>
</NativeSelectRoot>
</SimpleGrid>
</Card.Body>
</Card.Root>
) : (
<Flex justify="center" py={16}>
<Text color="fg.muted">{t("no-users")}</Text>
</Flex>
))}
{usersLoading ? (
<Flex justify="center" py={16}>
<Spinner size="lg" color="primary.500" />
</Flex>
) : users.length > 0 ? (
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
<Card.Body>
<VStack gap={0} align="stretch">
{/* Table Header */}
<Flex
px={4}
py={2}
bg="bg.muted"
borderRadius="lg"
mb={2}
fontWeight="semibold"
fontSize="xs"
color="fg.muted"
>
<Text flex={2}>{t("user-name")}</Text>
<Text flex={2}>{t("user-email")}</Text>
<Text flex={1} textAlign="center">
{t("user-role")}
</Text>
<Text flex={1} textAlign="center">
{t("subscription", { fallback: "Subscription" })}
</Text>
<Text flex={1} textAlign="center">
{t("user-status")}
</Text>
<Text width="40px" textAlign="center"></Text>
</Flex>
{/* User Rows */}
{users.map((user: AdminUserDto, idx: number) => (
<Box key={user.id ?? idx}>
{idx > 0 && <Separator />}
<Flex
px={4}
py={3}
align="center"
_hover={{ bg: "bg.muted" }}
borderRadius="lg"
>
<Text
flex={2}
fontSize="sm"
fontWeight="medium"
truncate
>
{getUserDisplayName(user)}
</Text>
<Text flex={2} fontSize="sm" color="fg.muted" truncate>
{user.email}
</Text>
<Flex flex={1} justify="center">
<Badge
colorPalette={
isAdminRole([user.role]) ? "red" : "gray"
}
variant="subtle"
fontSize="2xs"
borderRadius="full"
>
{formatRoleLabel(user.role)}
</Badge>
</Flex>
<Flex flex={1} justify="center" direction="column" align="center" gap={1}>
<Badge
colorPalette={user.subscriptionStatus === "premium" || user.subscriptionStatus === "plus" ? "purple" : "gray"}
variant="subtle"
fontSize="2xs"
borderRadius="full"
textTransform="capitalize"
>
{user.subscriptionStatus || "free"}
</Badge>
{user.subscriptionExpiresAt ? (
<Text fontSize="2xs" color="fg.muted">
{format.dateTime(new Date(user.subscriptionExpiresAt), { year: 'numeric', month: '2-digit', day: '2-digit' })}
</Text>
) : (
<Text fontSize="2xs" color="fg.muted">
-
</Text>
)}
</Flex>
<Flex flex={1} justify="center">
<Badge
colorPalette={
user.subscriptionStatus === "premium"
? "purple"
: user.subscriptionStatus === "plus"
? "blue"
: "gray"
}
variant="subtle"
fontSize="2xs"
borderRadius="full"
textTransform="capitalize"
>
{user.subscriptionStatus || "free"}
</Badge>
</Flex>
<Flex flex={1} justify="center">
<Badge
colorPalette={user.isActive ? "green" : "gray"}
variant="subtle"
fontSize="2xs"
borderRadius="full"
>
{user.isActive
? tCommon("active")
: tCommon("inactive")}
</Badge>
</Flex>
<Flex width="40px" justify="center">
<Button
size="sm"
variant="ghost"
onClick={() => setEditingUser(user)}
>
<LuPencil />
</Button>
</Flex>
</Flex>
</Box>
))}
{/* Pagination */}
{meta && meta.totalPages > 1 && (
<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 })}
>
Önceki
</Button>
<Flex align="center" gap={2} fontSize="sm">
<Text>Sayfa {meta.page} / {meta.totalPages}</Text>
</Flex>
<Button
size="sm"
variant="outline"
disabled={!meta.hasNextPage}
onClick={() => setSearchParams({ ...searchParams, page: meta.page + 1 })}
>
Sonraki
</Button>
</Flex>
)}
</VStack>
</Card.Body>
</Card.Root>
) : (
<Flex justify="center" py={16}>
<Text color="fg.muted">{t("no-users")}</Text>
</Flex>
)}
</VStack>
)}
<EditUserModal
user={editingUser}
isOpen={!!editingUser}
onClose={() => setEditingUser(null)}
/>
</Box>
</SlideUp>
);