diff --git a/app/admin/dashboard/area.tsx b/app/admin/dashboard/area.tsx
new file mode 100644
index 0000000..1e3c632
--- /dev/null
+++ b/app/admin/dashboard/area.tsx
@@ -0,0 +1,291 @@
+"use client"
+
+import * as React from "react"
+import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
+
+import { useIsMobile } from "@/hooks/use-mobile"
+import {
+ Card,
+ CardAction,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card"
+import {
+ ChartConfig,
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import {
+ ToggleGroup,
+ ToggleGroupItem,
+} from "@/components/ui/toggle-group"
+
+export const description = "An interactive area 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 },
+]
+
+const chartConfig = {
+ visitors: {
+ label: "Visitors",
+ },
+ desktop: {
+ label: "Desktop",
+ color: "var(--primary)",
+ },
+ mobile: {
+ label: "Mobile",
+ color: "var(--primary)",
+ },
+} satisfies ChartConfig
+
+export function ChartAreaInteractive() {
+ const isMobile = useIsMobile()
+ const [timeRange, setTimeRange] = React.useState("90d")
+
+ React.useEffect(() => {
+ if (isMobile) {
+ setTimeRange("7d")
+ }
+ }, [isMobile])
+
+ const filteredData = chartData.filter((item) => {
+ const date = new Date(item.date)
+ const referenceDate = new Date("2024-06-30")
+ let daysToSubtract = 90
+ if (timeRange === "30d") {
+ daysToSubtract = 30
+ } else if (timeRange === "7d") {
+ daysToSubtract = 7
+ }
+ const startDate = new Date(referenceDate)
+ startDate.setDate(startDate.getDate() - daysToSubtract)
+ return date >= startDate
+ })
+
+ return (
+
+
+ Total Visitors
+
+
+ Total for the last 3 months
+
+ Last 3 months
+
+
+
+ Last 3 months
+ Last 30 days
+ Last 7 days
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ const date = new Date(value)
+ return date.toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ })
+ }}
+ />
+ {
+ return new Date(value).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ })
+ }}
+ indicator="dot"
+ />
+ }
+ />
+
+
+
+
+
+
+ )
+}
diff --git a/app/admin/dashboard/page.tsx b/app/admin/dashboard/page.tsx
index 9b16337..430d24a 100644
--- a/app/admin/dashboard/page.tsx
+++ b/app/admin/dashboard/page.tsx
@@ -1,14 +1,23 @@
"use client"
+import { SectionCards } from "./sections"
+import { ChartAreaInteractive } from "./area"
+
export default function Page() {
return (
+
+
)
}
\ No newline at end of file
diff --git a/app/admin/dashboard/sections.tsx b/app/admin/dashboard/sections.tsx
new file mode 100644
index 0000000..25da063
--- /dev/null
+++ b/app/admin/dashboard/sections.tsx
@@ -0,0 +1,102 @@
+import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react"
+
+import { Badge } from "@/components/ui/badge"
+import {
+ Card,
+ CardAction,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card"
+
+export function SectionCards() {
+ return (
+
+
+
+ Total Revenue
+
+ $1,250.00
+
+
+
+
+ +12.5%
+
+
+
+
+
+ Trending up this month
+
+
+ Visitors for the last 6 months
+
+
+
+
+
+ New Customers
+
+ 1,234
+
+
+
+
+ -20%
+
+
+
+
+
+ Down 20% this period
+
+
+ Acquisition needs attention
+
+
+
+
+
+ Active Accounts
+
+ 45,678
+
+
+
+
+ +12.5%
+
+
+
+
+
+ Strong user retention
+
+ Engagement exceed targets
+
+
+
+
+ Growth Rate
+
+ 4.5%
+
+
+
+
+ +4.5%
+
+
+
+
+
+ Steady performance increase
+
+ Meets growth projections
+
+
+
+ )
+}
diff --git a/app/admin/page.tsx b/app/admin/page.tsx
index 4cbe2ed..7fc5425 100644
--- a/app/admin/page.tsx
+++ b/app/admin/page.tsx
@@ -20,6 +20,6 @@ export default function Dashboard() {
}
}, [isAuthenticated, isLoading, router, user]);
- return (<>>)
+ redirect('/admin/dashboard')
}
diff --git a/app/admin/users/user-table.tsx b/app/admin/users/user-table.tsx
index f19677d..00ce802 100644
--- a/app/admin/users/user-table.tsx
+++ b/app/admin/users/user-table.tsx
@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
-import { useQuery, gql, useMutation } from '@apollo/client';
+import { useQuery, gql } from '@apollo/client';
import {
Dialog,
DialogClose,
@@ -117,9 +117,10 @@ import {
TabsTrigger,
} from "@/components/ui/tabs"
import CreateUserForm from "./create-user-form";
+import { useUser } from "@/app/user-context";
export const schema = z.object({
- id: z.number(),
+ id: z.string(),
username: z.string(),
email: z.string(),
role: z.string(),
@@ -128,7 +129,7 @@ export const schema = z.object({
})
// Create a separate component for the drag handle
-function DragHandle({ id }: { id: number }) {
+function DragHandle({ id }: { id: string }) {
const { attributes, listeners } = useSortable({
id,
})
@@ -211,7 +212,21 @@ const columns: ColumnDef>[] = [
},
{
accessorKey: "createdAt",
- header: "Created At",
+ header: ({ column }) => {
+ return (
+
+ )
+ },
cell: ({ row }) => {
const date = new Date(row.original.createdAt);
const now = new Date();
@@ -252,10 +267,31 @@ const columns: ColumnDef>[] = [
);
},
+ enableSorting: true,
+ sortingFn: (rowA, rowB, columnId) => {
+ const dateA = new Date(rowA.original.createdAt);
+ const dateB = new Date(rowB.original.createdAt);
+ return dateA.getTime() - dateB.getTime();
+ },
},
{
- accessorKey: "lastLogin",
- header: "Last Login",
+ id: "lastLogin",
+ accessorFn: (row) => row.updatedAt,
+ header: ({ column }) => {
+ return (
+
+ )
+ },
cell: ({ row }) => {
const date = new Date(row.original.updatedAt);
const now = new Date();
@@ -292,6 +328,12 @@ const columns: ColumnDef>[] = [
);
},
+ enableSorting: true,
+ sortingFn: (rowA, rowB, columnId) => {
+ const dateA = new Date(rowA.original.updatedAt);
+ const dateB = new Date(rowB.original.updatedAt);
+ return dateA.getTime() - dateB.getTime();
+ },
},
{
id: "actions",
@@ -345,8 +387,8 @@ function DraggableRow({ row }: { row: Row> }) {
}
const GET_USERS = gql`
- query GetUsers($offset: Int, $limit: Int) {
- users(offset: $offset, limit: $limit) {
+ query GetUsers($offset: Int, $limit: Int, $sort_by: String, $sort_order: String, $filter: String) {
+ users(offset: $offset, limit: $limit, sortBy: $sort_by, sortOrder: $sort_order, filter: $filter) {
id
username
email
@@ -358,8 +400,8 @@ const GET_USERS = gql`
`
const USERS_INFO = gql`
- query UsersInfo($offset: Int, $limit: Int) {
- usersInfo(offset: $offset, limit: $limit) {
+ query UsersInfo($offset: Int, $limit: Int, $sort_by: String, $sort_order: String, $filter: String) {
+ usersInfo(offset: $offset, limit: $limit, sortBy: $sort_by, sortOrder: $sort_order, filter: $filter) {
totalUsers
totalActiveUsers
totalAdminUsers
@@ -378,7 +420,7 @@ const USERS_INFO = gql`
export function UserTable() {
- const { data, loading, error: usersInfoError, refetch } = useQuery(USERS_INFO)
+ const { data, loading, error, refetch } = useQuery(USERS_INFO)
const [localData, setLocalData] = React.useState([])
@@ -407,60 +449,20 @@ export function UserTable() {
return (
<>
-
+ {error && {error.message}
}
+
>
)
}
-function Tabel({ data, refetch, info, isLoading, handleDragEnd }: { data: any, refetch: any, info: any, isLoading: boolean, handleDragEnd: (event: DragEndEvent) => void }) {
-
- const [rowSelection, setRowSelection] = React.useState({})
- const [columnVisibility, setColumnVisibility] =
- React.useState({})
- const [columnFilters, setColumnFilters] = React.useState(
- []
- )
- const [sorting, setSorting] = React.useState([])
- const [pagination, setPagination] = React.useState({
- pageIndex: 0,
- pageSize: 10,
- })
- const sortableId = React.useId()
- const sensors = useSensors(
- useSensor(MouseSensor, {}),
- useSensor(TouchSensor, {}),
- useSensor(KeyboardSensor, {})
- )
-
-
- const table = useReactTable({
- data: data || [],
- columns,
- state: {
- sorting,
- columnVisibility,
- rowSelection,
- columnFilters,
- pagination,
- },
- getRowId: (row) => row.id.toString(),
- enableRowSelection: true,
- onRowSelectionChange: setRowSelection,
- onSortingChange: setSorting,
- onColumnFiltersChange: setColumnFilters,
- onColumnVisibilityChange: setColumnVisibility,
- onPaginationChange: setPagination,
- getCoreRowModel: getCoreRowModel(),
- getFilteredRowModel: getFilteredRowModel(),
- getPaginationRowModel: getPaginationRowModel(),
- getSortedRowModel: getSortedRowModel(),
- getFacetedRowModel: getFacetedRowModel(),
- getFacetedUniqueValues: getFacetedUniqueValues(),
- manualPagination: true,
- })
-
-
+function UserTabs({ initialData, initialLoading, info, handleDragEnd, refetch }: { initialData: any[], initialLoading: boolean, info: any, handleDragEnd: (event: DragEndEvent) => void, refetch: any }) {
return (
)
+}
+// 可重用的数据表格组件
+function UserDataTable({
+ data: propData,
+ isLoading: propIsLoading = false,
+ handleDragEnd,
+ filter,
+ useInitialData = false,
+ enableServerSideSort = false,
+ refetchFn
+}: {
+ data?: any[]
+ isLoading?: boolean
+ handleDragEnd: (event: DragEndEvent) => void
+ filter?: string
+ useInitialData?: boolean
+ enableServerSideSort?: boolean
+ refetchFn?: any
+}) {
+ const [localData, setLocalData] = React.useState([])
+ const [rowSelection, setRowSelection] = React.useState({})
+ const [columnVisibility, setColumnVisibility] = React.useState({})
+ const [columnFilters, setColumnFilters] = React.useState([])
+ const [sorting, setSorting] = React.useState([])
+ const [pagination, setPagination] = React.useState({
+ pageIndex: 0,
+ pageSize: 10,
+ })
+
+ // 为非初始数据的tab查询数据
+ const { data: queryData, loading: queryLoading, refetch } = useQuery(GET_USERS, {
+ variables: {
+ offset: pagination.pageIndex * pagination.pageSize,
+ limit: pagination.pageSize,
+ sortBy: sorting[0]?.id || "createdAt",
+ sortOrder: sorting[0]?.desc ? "DESC" : "ASC",
+ filter: filter
+ },
+ skip: useInitialData,
+ fetchPolicy: 'cache-and-network'
+ })
+
+ const data = useInitialData ? propData : queryData?.users
+ const isLoading = useInitialData ? propIsLoading : queryLoading
+
+ // 同步数据到本地状态
+ React.useEffect(() => {
+ if (data && Array.isArray(data)) {
+ setLocalData(data)
+ }
+ }, [data])
+
+ // 当筛选条件变化时重置分页
+ React.useEffect(() => {
+ setPagination({ pageIndex: 0, pageSize: 10 })
+ }, [filter])
+
+ // 当排序或分页变化时,重新查询数据
+ React.useEffect(() => {
+ const refetchFunc = refetchFn || refetch
+ if ((!useInitialData || enableServerSideSort) && refetchFunc) {
+ refetchFunc({
+ offset: pagination.pageIndex * pagination.pageSize,
+ limit: pagination.pageSize,
+ sortBy: sorting[0]?.id || "createdAt",
+ sortOrder: sorting[0]?.desc ? "DESC" : "ASC",
+ filter: filter
+ })
+ }
+ }, [sorting, pagination, filter, useInitialData, enableServerSideSort, refetch, refetchFn])
+
+ function handleLocalDragEnd(event: DragEndEvent) {
+ const { active, over } = event
+ if (active && over && active.id !== over.id) {
+ setLocalData((currentData) => {
+ const oldIndex = currentData.findIndex((item) => item.id === active.id)
+ const newIndex = currentData.findIndex((item) => item.id === over.id)
+
+ if (oldIndex === -1 || newIndex === -1) return currentData
+
+ // 只做本地排序,不保存到数据库
+ return arrayMove(currentData, oldIndex, newIndex)
+ })
+ }
+ // 也调用父组件的handleDragEnd
+ handleDragEnd(event)
+ }
+
+ const sortableId = React.useId()
+ const sensors = useSensors(
+ useSensor(MouseSensor, {}),
+ useSensor(TouchSensor, {}),
+ useSensor(KeyboardSensor, {})
+ )
+
+ const table = useReactTable({
+ data: localData || [],
+ columns,
+ state: {
+ sorting,
+ columnVisibility,
+ rowSelection,
+ columnFilters,
+ pagination,
+ },
+ getRowId: (row) => row.id.toString(),
+ enableRowSelection: true,
+ onRowSelectionChange: setRowSelection,
+ onSortingChange: setSorting,
+ onColumnFiltersChange: setColumnFilters,
+ onColumnVisibilityChange: setColumnVisibility,
+ onPaginationChange: setPagination,
+ getCoreRowModel: getCoreRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getFacetedRowModel: getFacetedRowModel(),
+ getFacetedUniqueValues: getFacetedUniqueValues(),
+ manualPagination: true,
+ })
+
+ return (
+ <>
+ {/*
+
+
+
+
+
+ {table
+ .getAllColumns()
+ .filter(
+ (column) =>
+ typeof column.accessorFn !== "undefined" &&
+ column.getCanHide()
+ )
+ .map((column) => {
+ return (
+
+ column.toggleVisibility(!!value)
+ }
+ >
+ {column.id}
+
+ )
+ })}
+
+
+
*/}
+
+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => {
+ return (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+
+ )
+ })}
+
+ ))}
+
+
+ {isLoading ? (
+
+
+ Loading...
+ {/* */}
+
+
+ ) : table.getRowModel().rows?.length ? (
+ user.id) || []}
+ strategy={verticalListSortingStrategy}
+ >
+ {table.getRowModel().rows.map((row) => (
+
+ ))}
+
+ ) : (
+
+
+ No results.
+
+
+ )}
+
+
+
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "}
+ {table.getFilteredRowModel().rows.length} row(s) selected.
+
+
+
+
+
+
+
+ Page {table.getState().pagination.pageIndex + 1} of{" "}
+ {table.getPageCount()}
+
+
+
+
+
+
+
+
+
+ >
+ )
}
const chartData = [
@@ -732,14 +879,26 @@ const chartConfig = {
function TableCellViewer({ item }: { item: z.infer }) {
const isMobile = useIsMobile()
+ const { user } = useUser()
return (
-
+
+
+ {
+ item.id === user?.id ? (
+
+ Me
+
+ ) : <>>
+ }
+
+
+
{item.username}
diff --git a/app/app-sidebar.tsx b/app/app-sidebar.tsx
index 38b6654..6139a3f 100644
--- a/app/app-sidebar.tsx
+++ b/app/app-sidebar.tsx
@@ -54,12 +54,8 @@ export function AppSidebar({ ...props }: React.ComponentProps) {
return (
- {
- user ? : null
- }
- {/* */}
-
-
-
-
-
-
- New Calendar
-
-
-
+ {
+ user ? : null
+ }
diff --git a/app/layout.tsx b/app/layout.tsx
index 739c029..5755905 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ClientProviders } from "./client-provider";
+import { Toaster } from "@/components/ui/sonner"
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -32,6 +33,7 @@ export default function RootLayout({
{children}
+