feat(record): add status card
This commit is contained in:
@@ -8,7 +8,12 @@ export default function DashboardRecordsLoading() {
|
||||
heading="Manage DNS Records"
|
||||
text="List and manage records"
|
||||
/>
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4 lg:grid-cols-4">
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
</div>
|
||||
<Skeleton className="h-[400px] w-full rounded-lg" />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { constructMetadata } from "@/lib/utils";
|
||||
import { DashboardHeader } from "@/components/dashboard/header";
|
||||
|
||||
import UserRecordsList from "../../dashboard/records/record-list";
|
||||
import UserRecordStatus from "../../dashboard/records/record-statu";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "DNS Records",
|
||||
@@ -24,6 +25,16 @@ export default async function DashboardPage() {
|
||||
link="/docs/dns-records"
|
||||
linkText="DNS records"
|
||||
/>
|
||||
<UserRecordStatus
|
||||
user={{
|
||||
id: user.id,
|
||||
name: user.name || "",
|
||||
apiKey: user.apiKey || "",
|
||||
email: user.email || "",
|
||||
role: user.role,
|
||||
}}
|
||||
action="/api/record/admin"
|
||||
/>
|
||||
<UserRecordsList
|
||||
user={{
|
||||
id: user.id,
|
||||
|
||||
@@ -327,7 +327,7 @@ export default function AppConfigs({}: {}) {
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<Textarea
|
||||
className="h-16 max-h-32 min-h-9 resize-y bg-white dark:bg-neutral-700"
|
||||
placeholder="1@a.com,2@b.com"
|
||||
placeholder="example1@wr.do,example2@wr.do"
|
||||
rows={5}
|
||||
// defaultValue={configs.catch_all_emails}
|
||||
value={catchAllEmails}
|
||||
|
||||
@@ -8,7 +8,12 @@ export default function DashboardRecordsLoading() {
|
||||
heading="Manage DNS Records"
|
||||
text="List and manage records"
|
||||
/>
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4 lg:grid-cols-4">
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
</div>
|
||||
<Skeleton className="h-[400px] w-full rounded-lg" />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { constructMetadata } from "@/lib/utils";
|
||||
import { DashboardHeader } from "@/components/dashboard/header";
|
||||
|
||||
import UserRecordsList from "./record-list";
|
||||
import UserRecordStatus from "./record-statu";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "DNS Records",
|
||||
@@ -24,6 +25,16 @@ export default async function DashboardPage() {
|
||||
link="/docs/dns-records"
|
||||
linkText="DNS records"
|
||||
/>
|
||||
<UserRecordStatus
|
||||
user={{
|
||||
id: user.id,
|
||||
name: user.name || "",
|
||||
apiKey: user.apiKey || "",
|
||||
email: user.email || "",
|
||||
role: user.role,
|
||||
}}
|
||||
action="/api/record"
|
||||
/>
|
||||
<UserRecordsList
|
||||
user={{
|
||||
id: user.id,
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
"use client";
|
||||
|
||||
import { User } from "@prisma/client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { fetcher, nFormatter } from "@/lib/utils";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import CountUp from "@/components/dashboard/count-up";
|
||||
|
||||
export interface RecordStatusProps {
|
||||
user: Pick<User, "id" | "name" | "apiKey" | "email" | "role">;
|
||||
action: string;
|
||||
}
|
||||
|
||||
export default function UserRecordStatus({ action }: RecordStatusProps) {
|
||||
const { data, isLoading, error } = useSWR<Record<string, number>>(
|
||||
`${action}/status`,
|
||||
fetcher,
|
||||
);
|
||||
|
||||
if (isLoading || error)
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4 lg:grid-cols-4">
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
<Skeleton className="h-[102px] w-full rounded-lg" />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
data && (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4 lg:grid-cols-4">
|
||||
<StatuInfoCard
|
||||
title="Active"
|
||||
total={data["total"]}
|
||||
monthTotal={data["active"]}
|
||||
/>
|
||||
<StatuInfoCard
|
||||
title="Inactive"
|
||||
total={data["total"]}
|
||||
monthTotal={data["inactive"]}
|
||||
/>
|
||||
<StatuInfoCard
|
||||
title="Pending"
|
||||
total={data["total"]}
|
||||
monthTotal={data["pending"]}
|
||||
/>
|
||||
<StatuInfoCard
|
||||
title="Rejected"
|
||||
total={data["total"]}
|
||||
monthTotal={data["rejected"]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function StatuInfoCard({
|
||||
title,
|
||||
total,
|
||||
monthTotal,
|
||||
}: {
|
||||
title: string;
|
||||
total?: number;
|
||||
monthTotal: number;
|
||||
}) {
|
||||
const t = useTranslations("Components");
|
||||
|
||||
const statusConfig = {
|
||||
Active: {
|
||||
color: "bg-green-500",
|
||||
textColor: "text-green-500",
|
||||
gradient: "from-green-400 to-green-600",
|
||||
shadow: "shadow-green-500/25",
|
||||
},
|
||||
Inactive: {
|
||||
color: "bg-gray-700",
|
||||
textColor: "text-gray-700",
|
||||
gradient: "from-gray-400 to-gray-600",
|
||||
shadow: "shadow-gray-500/25",
|
||||
},
|
||||
Pending: {
|
||||
color: "bg-yellow-500",
|
||||
textColor: "text-yellow-500",
|
||||
gradient: "from-yellow-400 to-yellow-600",
|
||||
shadow: "shadow-yellow-500/25",
|
||||
},
|
||||
Rejected: {
|
||||
color: "bg-red-500",
|
||||
textColor: "text-red-500",
|
||||
gradient: "from-red-400 to-red-600",
|
||||
shadow: "shadow-red-500/25",
|
||||
},
|
||||
};
|
||||
|
||||
const config = statusConfig[title];
|
||||
const percentage = total && total > 0 ? (monthTotal / total) * 100 : 0;
|
||||
const barHeight = Math.max(8, Math.min(100, percentage));
|
||||
|
||||
return (
|
||||
<Card className="grids group relative animate-fade-in overflow-hidden bg-gray-50/70 p-4 backdrop-blur-lg transition-all duration-300 hover:shadow-lg dark:bg-primary-foreground">
|
||||
<div className="flex items-end justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium">{t(title)}</div>
|
||||
<div className="mt-4 flex items-end gap-2 text-2xl font-bold">
|
||||
<CountUp count={monthTotal} />
|
||||
<p className="align-top text-base text-slate-500">
|
||||
/ {nFormatter(total || 0)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-4 flex h-16 items-end">
|
||||
<div className="relative flex flex-col items-center">
|
||||
{/* 柱状图背景 */}
|
||||
<div className="relative h-14 w-7 overflow-hidden rounded-lg border border-gray-300/20 bg-gray-200/50 backdrop-blur-sm dark:border-gray-600/20 dark:bg-gray-700/50">
|
||||
{/* 主柱状图 */}
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 bg-gradient-to-t ${config.gradient} rounded-lg transition-all duration-700 ease-out ${config.shadow}`}
|
||||
style={{ height: `${barHeight}%` }}
|
||||
/>
|
||||
{/* 光泽效果 */}
|
||||
<div
|
||||
className="absolute bottom-0 left-0 w-1/3 rounded-lg bg-gradient-to-t from-white/30 to-white/10 transition-all duration-700 ease-out"
|
||||
style={{ height: `${barHeight}%` }}
|
||||
/>
|
||||
{/* 顶部高光 */}
|
||||
{barHeight > 15 && (
|
||||
<div
|
||||
className="absolute left-0 right-0 h-1 rounded-t-lg bg-white/40"
|
||||
style={{ top: `${100 - barHeight}%` }}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={`absolute left-0 top-0.5 scale-75 text-xs font-semibold ${config.textColor}`}
|
||||
>
|
||||
{percentage.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
import { getDomainsByFeature } from "@/lib/dto/domains";
|
||||
import { getPlanQuota } from "@/lib/dto/plan";
|
||||
import { checkUserStatus, getUserByEmail } from "@/lib/dto/user";
|
||||
import { reservedDomains } from "@/lib/enums";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { generateSecret } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { getUserRecordStatus } from "@/lib/dto/cloudflare-dns-record";
|
||||
import { checkUserStatus } from "@/lib/dto/user";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const user = checkUserStatus(await getCurrentUser());
|
||||
if (user instanceof Response) return user;
|
||||
if (user.role !== "ADMIN") {
|
||||
return Response.json("Unauthorized", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
const status = await getUserRecordStatus(user.id, "ADMIN");
|
||||
|
||||
return Response.json(status);
|
||||
} catch (error) {
|
||||
return Response.json(error?.statusText || error, {
|
||||
status: error.status || 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { getUserRecordStatus } from "@/lib/dto/cloudflare-dns-record";
|
||||
import { checkUserStatus } from "@/lib/dto/user";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const user = checkUserStatus(await getCurrentUser());
|
||||
if (user instanceof Response) return user;
|
||||
|
||||
const status = await getUserRecordStatus(user.id, "USER");
|
||||
|
||||
return Response.json(status);
|
||||
} catch (error) {
|
||||
return Response.json(error?.statusText || error, {
|
||||
status: error.status || 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function StatusDot({ status }: { status: number }) {
|
||||
export default function StatusDot({
|
||||
status,
|
||||
color = "bg-green-500",
|
||||
}: {
|
||||
status: number;
|
||||
color?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-[9px] w-[9px] rounded-full",
|
||||
status === 1 ? "animate-pulse bg-green-500" : "bg-yellow-500",
|
||||
"h-[9px] w-[9px] animate-pulse rounded-full",
|
||||
status === 1 ? color : "bg-yellow-500",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -229,6 +229,45 @@ export async function getUserRecordCount(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserRecordStatus(
|
||||
userId: string,
|
||||
role: UserRole = "USER",
|
||||
) {
|
||||
const whereCondition = role === "USER" ? { userId } : {};
|
||||
|
||||
const statusCounts = await prisma.userRecord.groupBy({
|
||||
by: ["active"],
|
||||
where: whereCondition,
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
});
|
||||
|
||||
const total = await prisma.userRecord.count({
|
||||
where: whereCondition,
|
||||
});
|
||||
|
||||
const counts = statusCounts.reduce(
|
||||
(acc, item) => {
|
||||
// if (!item.active) {
|
||||
// acc[0] = item._count._all;
|
||||
// return acc;
|
||||
// }
|
||||
acc[item.active ?? 0] = item._count._all;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<number, number>,
|
||||
);
|
||||
|
||||
return {
|
||||
total,
|
||||
inactive: counts[0] || 0,
|
||||
active: counts[1] || 0,
|
||||
pending: counts[2] || 0,
|
||||
rejected: counts[3] || 0,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getUserRecordByTypeNameContent(
|
||||
userId: string,
|
||||
type: string,
|
||||
|
||||
+5
-1
@@ -300,7 +300,11 @@
|
||||
"All the time": "All the time",
|
||||
"No Visits": "No Visits",
|
||||
"You don't have any visits yet in": "You don't have any visits yet in",
|
||||
"System Settings": "System Settings"
|
||||
"System Settings": "System Settings",
|
||||
"Active": "Active",
|
||||
"Inactive": "Inactive",
|
||||
"Pending": "Pending",
|
||||
"Rejected": "Rejected"
|
||||
},
|
||||
"Landing": {
|
||||
"settings": "Settings",
|
||||
|
||||
+5
-1
@@ -300,7 +300,11 @@
|
||||
"All the time": "所有时间",
|
||||
"No Visits": "无访问记录",
|
||||
"You don't have any visits yet in": "您还没有任何访问记录于",
|
||||
"System Settings": "系统设置"
|
||||
"System Settings": "系统设置",
|
||||
"Active": "有效解析",
|
||||
"Inactive": "无效解析",
|
||||
"Pending": "审核中",
|
||||
"Rejected": "已拒绝"
|
||||
},
|
||||
"Landing": {
|
||||
"settings": "设置",
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user