feat: add admin charts

This commit is contained in:
oiov
2024-08-01 16:44:15 +08:00
parent 78c9f66292
commit ed6045e058
12 changed files with 146 additions and 199 deletions
+4 -4
View File
@@ -1,15 +1,13 @@
import { redirect } from "next/navigation";
import { siteConfig } from "@/config/site";
import { getUserRecordCount } from "@/lib/dto/cloudflare-dns-record";
import { getUserShortUrlCount } from "@/lib/dto/short-urls";
import { getAllUsersCount } from "@/lib/dto/user";
import { getCurrentUser } from "@/lib/session";
import { constructMetadata } from "@/lib/utils";
import { InteractiveBarChart } from "@/components/charts/interactive-bar-chart";
import { DashboardInfoCard } from "@/components/dashboard/dashboard-info-card";
import { DashboardHeader } from "@/components/dashboard/header";
import InfoCard from "@/components/dashboard/info-card";
import TransactionsList from "@/components/dashboard/transactions-list";
export const metadata = constructMetadata({
title: "Admin  WR.DO",
@@ -43,15 +41,17 @@ export default async function AdminPage() {
title="DNS Records"
count={record_count}
link="/admin/records"
icon="globeLock"
/>
<DashboardInfoCard
userId={user.id}
title="Short URLs"
count={url_count}
link="/admin/urls"
icon="link"
/>
</div>
<TransactionsList />
<InteractiveBarChart />
</div>
</>
);
@@ -1,11 +0,0 @@
import { Skeleton } from "@/components/ui/skeleton";
import { DashboardHeader } from "@/components/dashboard/header";
export default function ChartsLoading() {
return (
<>
<DashboardHeader heading="Charts" text="List of charts by shadcn-ui." />
<Skeleton className="size-full rounded-lg" />
</>
);
}
-41
View File
@@ -1,41 +0,0 @@
import { constructMetadata } from "@/lib/utils";
import { AreaChartStacked } from "@/components/charts/area-chart-stacked";
import { BarChartMixed } from "@/components/charts/bar-chart-mixed";
import { InteractiveBarChart } from "@/components/charts/interactive-bar-chart";
import { LineChartMultiple } from "@/components/charts/line-chart-multiple";
import { RadarChartSimple } from "@/components/charts/radar-chart-simple";
import { RadialChartGrid } from "@/components/charts/radial-chart-grid";
import { RadialShapeChart } from "@/components/charts/radial-shape-chart";
import { RadialStackedChart } from "@/components/charts/radial-stacked-chart";
import { RadialTextChart } from "@/components/charts/radial-text-chart";
import { DashboardHeader } from "@/components/dashboard/header";
export const metadata = constructMetadata({
title: "Charts  WR.DO",
description: "List of charts by shadcn-ui",
});
export default function ChartsPage() {
return (
<>
<DashboardHeader heading="Charts" text="List of charts by shadcn-ui." />
<div className="flex flex-col gap-5">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 2xl:grid-cols-4">
<RadialTextChart />
<AreaChartStacked />
<BarChartMixed />
<RadarChartSimple />
</div>
<InteractiveBarChart />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 2xl:grid-cols-4">
<RadialChartGrid />
<RadialShapeChart />
<LineChartMultiple />
<RadialStackedChart />
</div>
</div>
</>
);
}
+2
View File
@@ -43,6 +43,7 @@ export default async function DashboardPage() {
count={record_count}
total={siteConfig.freeQuota.record}
link="/dashboard/records"
icon="globeLock"
/>
<DashboardInfoCard
userId={user.id}
@@ -50,6 +51,7 @@ export default async function DashboardPage() {
count={url_count}
total={siteConfig.freeQuota.url}
link="/dashboard/urls"
icon="link"
/>
</div>
<UserRecordsList
@@ -79,8 +79,6 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
const { mutate } = useSWRConfig();
console.log("base", action);
const { data, error, isLoading } = useSWR<{
total: number;
list: UserRecordFormData[];
@@ -107,7 +105,15 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
<div className="grid gap-2">
<CardTitle>DNS Records</CardTitle>
<CardDescription className="text-balance">
Your DNS Records
See{" "}
<Link
target="_blank"
className="text-blue-500 hover:underline"
href="/docs/examples/vercel"
>
examples
</Link>{" "}
about how to use it.
</CardDescription>
</div>
)}
+92
View File
@@ -0,0 +1,92 @@
import { prisma } from "@/lib/db";
export async function GET(req: Request) {
try {
const threeMonthsAgo = new Date();
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
const users = await prisma.user.findMany({
where: {
createdAt: {
gte: threeMonthsAgo,
},
},
orderBy: {
createdAt: "desc",
},
select: {
createdAt: true,
},
});
const records = await prisma.userRecord.findMany({
where: {
created_on: {
gte: threeMonthsAgo,
},
},
orderBy: {
created_on: "desc",
},
select: {
created_on: true,
},
});
const urls = await prisma.userUrl.findMany({
where: {
createdAt: {
gte: threeMonthsAgo,
},
},
orderBy: {
createdAt: "desc",
},
select: {
createdAt: true,
},
});
const userCountByDate: { [date: string]: number } = {};
const recordCountByDate: { [date: string]: number } = {};
const urlCountByDate: { [date: string]: number } = {};
users.forEach((user) => {
const date = user.createdAt!.toISOString().split("T")[0];
userCountByDate[date] = (userCountByDate[date] || 0) + 1;
});
records.forEach((record) => {
const date = record.created_on!.toISOString().split("T")[0];
recordCountByDate[date] = (recordCountByDate[date] || 0) + 1;
});
urls.forEach((url) => {
const date = url.createdAt.toISOString().split("T")[0];
urlCountByDate[date] = (urlCountByDate[date] || 0) + 1;
});
const allDates = Array.from(
new Set([
...Object.keys(userCountByDate),
...Object.keys(recordCountByDate),
...Object.keys(urlCountByDate),
]),
);
const combinedData = allDates.map((date) => ({
date,
records: recordCountByDate[date] || 0,
urls: urlCountByDate[date] || 0,
users: userCountByDate[date] || 0,
}));
const total = {
records: combinedData.reduce((acc, curr) => acc + curr.records, 0),
urls: combinedData.reduce((acc, curr) => acc + curr.urls, 0),
users: combinedData.reduce((acc, curr) => acc + curr.users, 0),
};
return Response.json({ list: combinedData.reverse(), total });
} catch (error) {
return Response.json(error?.statusText || error, {
status: error.status || 500,
statusText: error.statusText || "Server error",
});
}
}
+28 -111
View File
@@ -2,7 +2,9 @@
import * as React from "react";
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
import useSWR from "swr";
import { fetcher } from "@/lib/utils";
import {
Card,
CardContent,
@@ -17,137 +19,52 @@ import {
ChartTooltipContent,
} from "@/components/ui/chart";
const chartData = [
{ date: "2024-04-01", desktop: 222, mobile: 150 },
{ date: "2024-04-02", desktop: 97, mobile: 180 },
{ date: "2024-04-03", desktop: 167, mobile: 120 },
{ date: "2024-04-04", desktop: 242, mobile: 260 },
{ date: "2024-04-05", desktop: 373, mobile: 290 },
{ date: "2024-04-06", desktop: 301, mobile: 340 },
{ date: "2024-04-07", desktop: 245, mobile: 180 },
{ date: "2024-04-08", desktop: 409, mobile: 320 },
{ date: "2024-04-09", desktop: 59, mobile: 110 },
{ date: "2024-04-10", desktop: 261, mobile: 190 },
{ date: "2024-04-11", desktop: 327, mobile: 350 },
{ date: "2024-04-12", desktop: 292, mobile: 210 },
{ date: "2024-04-13", desktop: 342, mobile: 380 },
{ date: "2024-04-14", desktop: 137, mobile: 220 },
{ date: "2024-04-15", desktop: 120, mobile: 170 },
{ date: "2024-04-16", desktop: 138, mobile: 190 },
{ date: "2024-04-17", desktop: 446, mobile: 360 },
{ date: "2024-04-18", desktop: 364, mobile: 410 },
{ date: "2024-04-19", desktop: 243, mobile: 180 },
{ date: "2024-04-20", desktop: 89, mobile: 150 },
{ date: "2024-04-21", desktop: 137, mobile: 200 },
{ date: "2024-04-22", desktop: 224, mobile: 170 },
{ date: "2024-04-23", desktop: 138, mobile: 230 },
{ date: "2024-04-24", desktop: 387, mobile: 290 },
{ date: "2024-04-25", desktop: 215, mobile: 250 },
{ date: "2024-04-26", desktop: 75, mobile: 130 },
{ date: "2024-04-27", desktop: 383, mobile: 420 },
{ date: "2024-04-28", desktop: 122, mobile: 180 },
{ date: "2024-04-29", desktop: 315, mobile: 240 },
{ date: "2024-04-30", desktop: 454, mobile: 380 },
{ date: "2024-05-01", desktop: 165, mobile: 220 },
{ date: "2024-05-02", desktop: 293, mobile: 310 },
{ date: "2024-05-03", desktop: 247, mobile: 190 },
{ date: "2024-05-04", desktop: 385, mobile: 420 },
{ date: "2024-05-05", desktop: 481, mobile: 390 },
{ date: "2024-05-06", desktop: 498, mobile: 520 },
{ date: "2024-05-07", desktop: 388, mobile: 300 },
{ date: "2024-05-08", desktop: 149, mobile: 210 },
{ date: "2024-05-09", desktop: 227, mobile: 180 },
{ date: "2024-05-10", desktop: 293, mobile: 330 },
{ date: "2024-05-11", desktop: 335, mobile: 270 },
{ date: "2024-05-12", desktop: 197, mobile: 240 },
{ date: "2024-05-13", desktop: 197, mobile: 160 },
{ date: "2024-05-14", desktop: 448, mobile: 490 },
{ date: "2024-05-15", desktop: 473, mobile: 380 },
{ date: "2024-05-16", desktop: 338, mobile: 400 },
{ date: "2024-05-17", desktop: 499, mobile: 420 },
{ date: "2024-05-18", desktop: 315, mobile: 350 },
{ date: "2024-05-19", desktop: 235, mobile: 180 },
{ date: "2024-05-20", desktop: 177, mobile: 230 },
{ date: "2024-05-21", desktop: 82, mobile: 140 },
{ date: "2024-05-22", desktop: 81, mobile: 120 },
{ date: "2024-05-23", desktop: 252, mobile: 290 },
{ date: "2024-05-24", desktop: 294, mobile: 220 },
{ date: "2024-05-25", desktop: 201, mobile: 250 },
{ date: "2024-05-26", desktop: 213, mobile: 170 },
{ date: "2024-05-27", desktop: 420, mobile: 460 },
{ date: "2024-05-28", desktop: 233, mobile: 190 },
{ date: "2024-05-29", desktop: 78, mobile: 130 },
{ date: "2024-05-30", desktop: 340, mobile: 280 },
{ date: "2024-05-31", desktop: 178, mobile: 230 },
{ date: "2024-06-01", desktop: 178, mobile: 200 },
{ date: "2024-06-02", desktop: 470, mobile: 410 },
{ date: "2024-06-03", desktop: 103, mobile: 160 },
{ date: "2024-06-04", desktop: 439, mobile: 380 },
{ date: "2024-06-05", desktop: 88, mobile: 140 },
{ date: "2024-06-06", desktop: 294, mobile: 250 },
{ date: "2024-06-07", desktop: 323, mobile: 370 },
{ date: "2024-06-08", desktop: 385, mobile: 320 },
{ date: "2024-06-09", desktop: 438, mobile: 480 },
{ date: "2024-06-10", desktop: 155, mobile: 200 },
{ date: "2024-06-11", desktop: 92, mobile: 150 },
{ date: "2024-06-12", desktop: 492, mobile: 420 },
{ date: "2024-06-13", desktop: 81, mobile: 130 },
{ date: "2024-06-14", desktop: 426, mobile: 380 },
{ date: "2024-06-15", desktop: 307, mobile: 350 },
{ date: "2024-06-16", desktop: 371, mobile: 310 },
{ date: "2024-06-17", desktop: 475, mobile: 520 },
{ date: "2024-06-18", desktop: 107, mobile: 170 },
{ date: "2024-06-19", desktop: 341, mobile: 290 },
{ date: "2024-06-20", desktop: 408, mobile: 450 },
{ date: "2024-06-21", desktop: 169, mobile: 210 },
{ date: "2024-06-22", desktop: 317, mobile: 270 },
{ date: "2024-06-23", desktop: 480, mobile: 530 },
{ date: "2024-06-24", desktop: 132, mobile: 180 },
{ date: "2024-06-25", desktop: 141, mobile: 190 },
{ date: "2024-06-26", desktop: 434, mobile: 380 },
{ date: "2024-06-27", desktop: 448, mobile: 490 },
{ date: "2024-06-28", desktop: 149, mobile: 200 },
{ date: "2024-06-29", desktop: 103, mobile: 160 },
{ date: "2024-06-30", desktop: 446, mobile: 400 },
];
import { Skeleton } from "../ui/skeleton";
const chartConfig = {
views: {
label: "Page Views",
label: "Create",
},
desktop: {
label: "Desktop",
records: {
label: "Records",
color: "hsl(var(--chart-1))",
},
mobile: {
label: "Mobile",
urls: {
label: "URLs",
color: "hsl(var(--chart-2))",
},
users: {
label: "Users",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig;
export function InteractiveBarChart() {
const [activeChart, setActiveChart] =
React.useState<keyof typeof chartConfig>("desktop");
React.useState<keyof typeof chartConfig>("users");
const total = React.useMemo(
() => ({
desktop: chartData.reduce((acc, curr) => acc + curr.desktop, 0),
mobile: chartData.reduce((acc, curr) => acc + curr.mobile, 0),
}),
[],
);
const { data, isLoading } = useSWR<{
list: [{ records: number; urls: number; users: number; date: string }];
total: { records: number; urls: number; users: number };
}>(`api/admin`, fetcher, {
revalidateOnFocus: false,
});
if (isLoading) return <Skeleton className="size-full rounded-lg" />;
if (!data) return null;
return (
<Card>
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
<CardTitle>Bar Chart - Interactive</CardTitle>
<CardTitle>Data Increase</CardTitle>
<CardDescription>
Showing total visitors for the last 3 months
Showing total data for the last 3 months
</CardDescription>
</div>
<div className="flex">
{["desktop", "mobile"].map((key) => {
{["users", "records", "urls"].map((key) => {
const chart = key as keyof typeof chartConfig;
return (
<button
@@ -160,7 +77,7 @@ export function InteractiveBarChart() {
{chartConfig[chart].label}
</span>
<span className="text-lg font-bold leading-none sm:text-3xl">
{total[key as keyof typeof total].toLocaleString()}
{data.total[key as keyof typeof data.total].toLocaleString()}
</span>
</button>
);
@@ -174,7 +91,7 @@ export function InteractiveBarChart() {
>
<BarChart
accessibilityLayer
data={chartData}
data={data.list}
margin={{
left: 12,
right: 12,
+4 -5
View File
@@ -12,16 +12,16 @@ export async function DashboardInfoCard({
total,
count,
link,
// icon = "user",
icon = "users",
}: {
userId: string;
title: string;
total?: number;
count: number;
link: string;
// icon: keyof typeof Icons;
icon?: keyof typeof Icons;
}) {
// const iconCpn = Icons[icon];
const Icon = Icons[icon || "arrowRight"];
return (
<Card className="grids group bg-gray-50/70 backdrop-blur-lg dark:bg-primary-foreground">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
@@ -33,8 +33,7 @@ export async function DashboardInfoCard({
{title}
</Link>
</CardTitle>
{/* {icon && <iconCpn />} */}
<LinkIcon className="size-4 text-muted-foreground" />
<Icon className="size-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{[-1, undefined].includes(count) ? (
-19
View File
@@ -29,25 +29,6 @@ interface DashboardSidebarProps {
export function DashboardSidebar({ links }: DashboardSidebarProps) {
const path = usePathname();
// NOTE: Use this if you want save in local storage -- Credits: Hosna Qasmei
//
// const [isSidebarExpanded, setIsSidebarExpanded] = useState(() => {
// if (typeof window !== "undefined") {
// const saved = window.localStorage.getItem("sidebarExpanded");
// return saved !== null ? JSON.parse(saved) : true;
// }
// return true;
// });
// useEffect(() => {
// if (typeof window !== "undefined") {
// window.localStorage.setItem(
// "sidebarExpanded",
// JSON.stringify(isSidebarExpanded),
// );
// }
// }, [isSidebarExpanded]);
const { isTablet } = useMediaQuery();
const [isSidebarExpanded, setIsSidebarExpanded] = useState(!isTablet);
+2
View File
@@ -32,6 +32,7 @@ import {
SunMedium,
Trash2,
User,
Users,
X,
} from "lucide-react";
@@ -119,6 +120,7 @@ export const Icons = {
</svg>
),
user: User,
users: Users,
warning: AlertTriangle,
globeLock: GlobeLock,
link: Link,
+4 -4
View File
@@ -24,20 +24,20 @@ export const sidebarLinks: SidebarNavItem[] = [
},
{
href: "/admin/users",
icon: "user",
title: "Users",
icon: "users",
title: "User List",
authorizeOnly: UserRole.ADMIN,
},
{
href: "/admin/records",
icon: "lineChart",
title: "Records",
title: "Record List",
authorizeOnly: UserRole.ADMIN,
},
{
href: "/admin/urls",
icon: "post",
title: "URLs",
title: "URL List",
authorizeOnly: UserRole.ADMIN,
},
],
+1 -1
View File
@@ -100,7 +100,7 @@ export const expirationTime = (
const remainingTime = expirationTime - now;
if (remainingTime <= 0) return "Expired";
const remainingTimeString = ms(remainingTime, { long: false });
const remainingTimeString = ms(remainingTime, { long: true });
if (timeOnly) {
return remainingTimeString;
}