From ed6045e058584731dbf5bccb8a93b30771f79f87 Mon Sep 17 00:00:00 2001 From: oiov Date: Thu, 1 Aug 2024 16:44:15 +0800 Subject: [PATCH] feat: add admin charts --- app/(protected)/admin/page.tsx | 8 +- app/(protected)/dashboard/charts/loading.tsx | 11 -- app/(protected)/dashboard/charts/page.tsx | 41 ------ app/(protected)/dashboard/page.tsx | 2 + .../dashboard/records/record-list.tsx | 12 +- app/api/admin/route.ts | 92 ++++++++++++ components/charts/interactive-bar-chart.tsx | 139 ++++-------------- components/dashboard/dashboard-info-card.tsx | 9 +- components/layout/dashboard-sidebar.tsx | 19 --- components/shared/icons.tsx | 2 + config/dashboard.ts | 8 +- lib/utils.ts | 2 +- 12 files changed, 146 insertions(+), 199 deletions(-) delete mode 100644 app/(protected)/dashboard/charts/loading.tsx delete mode 100644 app/(protected)/dashboard/charts/page.tsx create mode 100644 app/api/admin/route.ts diff --git a/app/(protected)/admin/page.tsx b/app/(protected)/admin/page.tsx index 68776a6..c3c6d9a 100644 --- a/app/(protected)/admin/page.tsx +++ b/app/(protected)/admin/page.tsx @@ -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" /> - + ); diff --git a/app/(protected)/dashboard/charts/loading.tsx b/app/(protected)/dashboard/charts/loading.tsx deleted file mode 100644 index 2640f41..0000000 --- a/app/(protected)/dashboard/charts/loading.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Skeleton } from "@/components/ui/skeleton"; -import { DashboardHeader } from "@/components/dashboard/header"; - -export default function ChartsLoading() { - return ( - <> - - - - ); -} diff --git a/app/(protected)/dashboard/charts/page.tsx b/app/(protected)/dashboard/charts/page.tsx deleted file mode 100644 index 046a15e..0000000 --- a/app/(protected)/dashboard/charts/page.tsx +++ /dev/null @@ -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 ( - <> - -
-
- - - - -
- - - -
- - - - -
-
- - ); -} diff --git a/app/(protected)/dashboard/page.tsx b/app/(protected)/dashboard/page.tsx index 4d95ad8..e5a4fab 100644 --- a/app/(protected)/dashboard/page.tsx +++ b/app/(protected)/dashboard/page.tsx @@ -43,6 +43,7 @@ export default async function DashboardPage() { count={record_count} total={siteConfig.freeQuota.record} link="/dashboard/records" + icon="globeLock" /> DNS Records - Your DNS Records + See{" "} + + examples + {" "} + about how to use it. )} diff --git a/app/api/admin/route.ts b/app/api/admin/route.ts new file mode 100644 index 0000000..686374e --- /dev/null +++ b/app/api/admin/route.ts @@ -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", + }); + } +} diff --git a/components/charts/interactive-bar-chart.tsx b/components/charts/interactive-bar-chart.tsx index 1690324..3550b0b 100644 --- a/components/charts/interactive-bar-chart.tsx +++ b/components/charts/interactive-bar-chart.tsx @@ -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("desktop"); + React.useState("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 ; + + if (!data) return null; return (
- Bar Chart - Interactive + Data Increase - Showing total visitors for the last 3 months + Showing total data for the last 3 months
- {["desktop", "mobile"].map((key) => { + {["users", "records", "urls"].map((key) => { const chart = key as keyof typeof chartConfig; return ( ); @@ -174,7 +91,7 @@ export function InteractiveBarChart() { > @@ -33,8 +33,7 @@ export async function DashboardInfoCard({ {title} - {/* {icon && } */} - + {[-1, undefined].includes(count) ? ( diff --git a/components/layout/dashboard-sidebar.tsx b/components/layout/dashboard-sidebar.tsx index 0953cdd..5df8889 100644 --- a/components/layout/dashboard-sidebar.tsx +++ b/components/layout/dashboard-sidebar.tsx @@ -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); diff --git a/components/shared/icons.tsx b/components/shared/icons.tsx index 5996a4e..5996f1d 100644 --- a/components/shared/icons.tsx +++ b/components/shared/icons.tsx @@ -32,6 +32,7 @@ import { SunMedium, Trash2, User, + Users, X, } from "lucide-react"; @@ -119,6 +120,7 @@ export const Icons = { ), user: User, + users: Users, warning: AlertTriangle, globeLock: GlobeLock, link: Link, diff --git a/config/dashboard.ts b/config/dashboard.ts index 7aa582b..1dfd40b 100644 --- a/config/dashboard.ts +++ b/config/dashboard.ts @@ -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, }, ], diff --git a/lib/utils.ts b/lib/utils.ts index 7f315f3..0bf8a58 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -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; }