833 lines
33 KiB
TypeScript
833 lines
33 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { useQuery, gql, useMutation } from '@apollo/client';
|
|
import {
|
|
Dialog,
|
|
DialogClose,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog"
|
|
import {
|
|
closestCenter,
|
|
DndContext,
|
|
KeyboardSensor,
|
|
MouseSensor,
|
|
TouchSensor,
|
|
useSensor,
|
|
useSensors,
|
|
type DragEndEvent,
|
|
type UniqueIdentifier,
|
|
} from "@dnd-kit/core"
|
|
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
|
|
import {
|
|
arrayMove,
|
|
SortableContext,
|
|
useSortable,
|
|
verticalListSortingStrategy,
|
|
} from "@dnd-kit/sortable"
|
|
import { CSS } from "@dnd-kit/utilities"
|
|
import {
|
|
IconChevronDown,
|
|
IconChevronLeft,
|
|
IconChevronRight,
|
|
IconChevronsLeft,
|
|
IconChevronsRight,
|
|
IconCircleCheckFilled,
|
|
IconDotsVertical,
|
|
IconGripVertical,
|
|
IconLayoutColumns,
|
|
IconLoader,
|
|
IconPlus,
|
|
IconTrendingUp,
|
|
} from "@tabler/icons-react"
|
|
import {
|
|
ColumnDef,
|
|
ColumnFiltersState,
|
|
flexRender,
|
|
getCoreRowModel,
|
|
getFacetedRowModel,
|
|
getFacetedUniqueValues,
|
|
getFilteredRowModel,
|
|
getPaginationRowModel,
|
|
getSortedRowModel,
|
|
Row,
|
|
SortingState,
|
|
useReactTable,
|
|
VisibilityState,
|
|
} from "@tanstack/react-table"
|
|
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
|
|
import { toast } from "sonner"
|
|
import { z } from "zod"
|
|
|
|
import { useIsMobile } from "@/hooks/use-mobile"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
ChartConfig,
|
|
ChartContainer,
|
|
ChartTooltip,
|
|
ChartTooltipContent,
|
|
} from "@/components/ui/chart"
|
|
import { Checkbox } from "@/components/ui/checkbox"
|
|
import {
|
|
Drawer,
|
|
DrawerClose,
|
|
DrawerContent,
|
|
DrawerDescription,
|
|
DrawerFooter,
|
|
DrawerHeader,
|
|
DrawerTitle,
|
|
DrawerTrigger,
|
|
} from "@/components/ui/drawer"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuCheckboxItem,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
import { Separator } from "@/components/ui/separator"
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table"
|
|
import {
|
|
Tabs,
|
|
TabsContent,
|
|
TabsList,
|
|
TabsTrigger,
|
|
} from "@/components/ui/tabs"
|
|
import CreateUserForm from "./create-user-form";
|
|
|
|
export const schema = z.object({
|
|
id: z.number(),
|
|
username: z.string(),
|
|
email: z.string(),
|
|
role: z.string(),
|
|
createdAt: z.string(),
|
|
updatedAt: z.string(),
|
|
})
|
|
|
|
// Create a separate component for the drag handle
|
|
function DragHandle({ id }: { id: number }) {
|
|
const { attributes, listeners } = useSortable({
|
|
id,
|
|
})
|
|
|
|
return (
|
|
<Button
|
|
{...attributes}
|
|
{...listeners}
|
|
variant="ghost"
|
|
size="icon"
|
|
className="text-muted-foreground size-7 hover:bg-transparent"
|
|
>
|
|
<IconGripVertical className="text-muted-foreground size-3" />
|
|
<span className="sr-only">Drag to reorder</span>
|
|
</Button>
|
|
)
|
|
}
|
|
|
|
const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
|
{
|
|
id: "drag",
|
|
header: () => null,
|
|
cell: ({ row }) => <DragHandle id={row.original.id} />,
|
|
},
|
|
{
|
|
id: "select",
|
|
header: ({ table }) => (
|
|
<div className="flex items-center justify-center">
|
|
<Checkbox
|
|
checked={
|
|
table.getIsAllPageRowsSelected() ||
|
|
(table.getIsSomePageRowsSelected() && "indeterminate")
|
|
}
|
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
|
aria-label="Select all"
|
|
/>
|
|
</div>
|
|
),
|
|
cell: ({ row }) => (
|
|
<div className="flex items-center justify-center">
|
|
<Checkbox
|
|
checked={row.getIsSelected()}
|
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
aria-label="Select row"
|
|
/>
|
|
</div>
|
|
),
|
|
enableSorting: false,
|
|
enableHiding: false,
|
|
},
|
|
{
|
|
accessorKey: "name",
|
|
header: "Name",
|
|
cell: ({ row }) => {
|
|
return <TableCellViewer item={row.original} />
|
|
},
|
|
enableHiding: false,
|
|
},
|
|
{
|
|
accessorKey: "email",
|
|
header: "Email",
|
|
cell: ({ row }) => (
|
|
<div className="w-48">
|
|
<span className="text-sm text-muted-foreground">
|
|
{row.original.email}
|
|
</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: "role",
|
|
header: "Role",
|
|
cell: ({ row }) => (
|
|
<div className="w-32">
|
|
<Badge variant="outline" className="text-muted-foreground px-1.5">
|
|
{row.original.role}
|
|
</Badge>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: "createdAt",
|
|
header: "Created At",
|
|
cell: ({ row }) => {
|
|
const date = new Date(row.original.createdAt);
|
|
const now = new Date();
|
|
const diffInDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
|
let timeAgo = '';
|
|
if (diffInDays === 0) {
|
|
timeAgo = 'Today';
|
|
} else if (diffInDays === 1) {
|
|
timeAgo = 'Yesterday';
|
|
} else if (diffInDays < 7) {
|
|
timeAgo = `${diffInDays} days ago`;
|
|
} else if (diffInDays < 30) {
|
|
const weeks = Math.floor(diffInDays / 7);
|
|
timeAgo = `${weeks} week${weeks > 1 ? 's' : ''} ago`;
|
|
} else if (diffInDays < 365) {
|
|
const months = Math.floor(diffInDays / 30);
|
|
timeAgo = `${months} month${months > 1 ? 's' : ''} ago`;
|
|
} else {
|
|
const years = Math.floor(diffInDays / 365);
|
|
timeAgo = `${years} year${years > 1 ? 's' : ''} ago`;
|
|
}
|
|
|
|
return (
|
|
<div className="w-40">
|
|
<div className="flex flex-row gap-2">
|
|
<span className="text-sm font-medium">
|
|
{date.toLocaleDateString('zh-CN', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
})}
|
|
</span>
|
|
<Badge variant="secondary" className="text-xs w-fit">
|
|
{timeAgo}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "lastLogin",
|
|
header: "Last Login",
|
|
cell: ({ row }) => {
|
|
const date = new Date(row.original.updatedAt);
|
|
const now = new Date();
|
|
const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60));
|
|
|
|
let timeAgo = '';
|
|
if (diffInMinutes < 1) {
|
|
timeAgo = 'Just now';
|
|
} else if (diffInMinutes < 60) {
|
|
timeAgo = `${diffInMinutes} min${diffInMinutes > 1 ? 's' : ''} ago`;
|
|
} else if (diffInMinutes < 1440) { // 24 hours
|
|
const hours = Math.floor(diffInMinutes / 60);
|
|
timeAgo = `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
|
} else {
|
|
const days = Math.floor(diffInMinutes / 1440);
|
|
timeAgo = `${days} day${days > 1 ? 's' : ''} ago`;
|
|
}
|
|
|
|
return (
|
|
<div className="w-40">
|
|
<div className="flex flex-row gap-2">
|
|
<span className="text-sm font-medium">
|
|
{date.toLocaleDateString('zh-CN', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})}
|
|
</span>
|
|
<Badge variant="outline" className="text-xs w-fit">
|
|
{timeAgo}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: "actions",
|
|
cell: () => (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
className="data-[state=open]:bg-muted text-muted-foreground flex size-8"
|
|
size="icon"
|
|
>
|
|
<IconDotsVertical />
|
|
<span className="sr-only">Open menu</span>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-32">
|
|
<DropdownMenuItem>Edit</DropdownMenuItem>
|
|
<DropdownMenuItem>Make a copy</DropdownMenuItem>
|
|
<DropdownMenuItem>Favorite</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
),
|
|
},
|
|
]
|
|
|
|
function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
|
|
const { transform, transition, setNodeRef, isDragging } = useSortable({
|
|
id: row.original.id,
|
|
})
|
|
|
|
return (
|
|
<TableRow
|
|
data-state={row.getIsSelected() && "selected"}
|
|
data-dragging={isDragging}
|
|
ref={setNodeRef}
|
|
className="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
|
|
style={{
|
|
transform: CSS.Transform.toString(transform),
|
|
transition: transition,
|
|
}}
|
|
>
|
|
{row.getVisibleCells().map((cell) => (
|
|
<TableCell key={cell.id}>
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
)
|
|
}
|
|
|
|
const GET_USERS = gql`
|
|
query GetUsers($offset: Int, $limit: Int) {
|
|
users(offset: $offset, limit: $limit) {
|
|
id
|
|
username
|
|
email
|
|
role
|
|
createdAt
|
|
updatedAt
|
|
}
|
|
}
|
|
`
|
|
|
|
const USERS_INFO = gql`
|
|
query UsersInfo {
|
|
usersInfo {
|
|
totalUsers
|
|
totalActiveUsers
|
|
totalAdminUsers
|
|
totalUserUsers
|
|
}
|
|
}
|
|
`
|
|
|
|
export function UserTable() {
|
|
|
|
const { data, loading, error, refetch } = useQuery(GET_USERS, {
|
|
variables: {
|
|
offset: 0,
|
|
limit: 10,
|
|
},
|
|
})
|
|
|
|
const { data: usersInfo, loading: usersInfoLoading, error: usersInfoError } = useQuery(USERS_INFO)
|
|
|
|
return (
|
|
<>
|
|
{loading && <div>Loading...</div>}
|
|
{error && <div>Error: {error.message}</div>}
|
|
{data && <Tabel data={data} info={usersInfo} refetch={refetch} />}
|
|
</>
|
|
)
|
|
|
|
|
|
}
|
|
|
|
function Tabel({ data, refetch, info }: { data: any, refetch: any, info: any }) {
|
|
|
|
const [rowSelection, setRowSelection] = React.useState({})
|
|
const [columnVisibility, setColumnVisibility] =
|
|
React.useState<VisibilityState>({})
|
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
|
[]
|
|
)
|
|
const [sorting, setSorting] = React.useState<SortingState>([])
|
|
const [pagination, setPagination] = React.useState({
|
|
pageIndex: 0,
|
|
pageSize: 10,
|
|
})
|
|
const sortableId = React.useId()
|
|
const sensors = useSensors(
|
|
useSensor(MouseSensor, {}),
|
|
useSensor(TouchSensor, {}),
|
|
useSensor(KeyboardSensor, {})
|
|
)
|
|
|
|
React.useEffect(() => {
|
|
refetch({
|
|
offset: pagination.pageIndex * pagination.pageSize,
|
|
limit: pagination.pageSize,
|
|
})
|
|
}, [pagination, refetch])
|
|
|
|
const table = useReactTable({
|
|
data: data?.users || [],
|
|
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 handleDragEnd(event: DragEndEvent) {
|
|
// const { active, over } = event
|
|
// if (active && over && active.id !== over.id) {
|
|
// setData((data) => {
|
|
// const oldIndex = dataIds.indexOf(active.id)
|
|
// const newIndex = dataIds.indexOf(over.id)
|
|
// return arrayMove(data, oldIndex, newIndex)
|
|
// })
|
|
// }
|
|
// }
|
|
|
|
return (
|
|
<Dialog>
|
|
<DialogContent className="sm:max-w-[500px]">
|
|
<DialogHeader>
|
|
<DialogTitle>添加新用户</DialogTitle>
|
|
<DialogDescription>
|
|
创建新用户账户。系统将自动生成随机密码。
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<CreateUserForm />
|
|
</DialogContent>
|
|
|
|
<Tabs
|
|
defaultValue="outline"
|
|
className="w-full flex-col justify-start gap-6"
|
|
>
|
|
<div className="flex items-center justify-between px-4 lg:px-6">
|
|
<Label htmlFor="view-selector" className="sr-only">
|
|
View
|
|
</Label>
|
|
<Select defaultValue="outline">
|
|
<SelectTrigger
|
|
className="flex w-fit @4xl/main:hidden"
|
|
size="sm"
|
|
id="view-selector"
|
|
>
|
|
<SelectValue placeholder="Select a view" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="outline">All Users</SelectItem>
|
|
<SelectItem value="past-performance">Active Users</SelectItem>
|
|
<SelectItem value="key-personnel">Administrators</SelectItem>
|
|
<SelectItem value="focus-documents">Inactive Users</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<TabsList className="**:data-[slot=badge]:bg-muted-foreground/30 hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:px-1 @4xl/main:flex">
|
|
<TabsTrigger value="outline">All Users</TabsTrigger>
|
|
<TabsTrigger value="past-performance">
|
|
Active Users <Badge variant="secondary">{info?.usersInfo?.totalActiveUsers}</Badge>
|
|
</TabsTrigger>
|
|
<TabsTrigger value="key-personnel">
|
|
Administrators <Badge variant="secondary">{info?.usersInfo?.totalAdminUsers}</Badge>
|
|
</TabsTrigger>
|
|
<TabsTrigger value="focus-documents">Inactive Users</TabsTrigger>
|
|
</TabsList>
|
|
<div className="flex items-center gap-2">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" size="sm">
|
|
<IconLayoutColumns />
|
|
<span className="hidden lg:inline">Customize Columns</span>
|
|
<span className="lg:hidden">Columns</span>
|
|
<IconChevronDown />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-56">
|
|
{table
|
|
.getAllColumns()
|
|
.filter(
|
|
(column) =>
|
|
typeof column.accessorFn !== "undefined" &&
|
|
column.getCanHide()
|
|
)
|
|
.map((column) => {
|
|
return (
|
|
<DropdownMenuCheckboxItem
|
|
key={column.id}
|
|
className="capitalize"
|
|
checked={column.getIsVisible()}
|
|
onCheckedChange={(value) =>
|
|
column.toggleVisibility(!!value)
|
|
}
|
|
>
|
|
{column.id}
|
|
</DropdownMenuCheckboxItem>
|
|
)
|
|
})}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
<DialogTrigger asChild>
|
|
<Button variant="outline" size="sm">
|
|
<IconPlus />
|
|
<span className="hidden lg:inline">Add User</span>
|
|
</Button>
|
|
</DialogTrigger>
|
|
|
|
</div>
|
|
</div>
|
|
<TabsContent
|
|
value="outline"
|
|
className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"
|
|
>
|
|
<div className="overflow-hidden rounded-lg border">
|
|
<DndContext
|
|
collisionDetection={closestCenter}
|
|
modifiers={[restrictToVerticalAxis]}
|
|
// onDragEnd={handleDragEnd}
|
|
sensors={sensors}
|
|
id={sortableId}
|
|
>
|
|
<Table>
|
|
<TableHeader className="bg-muted sticky top-0 z-10">
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<TableRow key={headerGroup.id}>
|
|
{headerGroup.headers.map((header) => {
|
|
return (
|
|
<TableHead key={header.id} colSpan={header.colSpan}>
|
|
{header.isPlaceholder
|
|
? null
|
|
: flexRender(
|
|
header.column.columnDef.header,
|
|
header.getContext()
|
|
)}
|
|
</TableHead>
|
|
)
|
|
})}
|
|
</TableRow>
|
|
))}
|
|
</TableHeader>
|
|
<TableBody className="**:data-[slot=table-cell]:first:w-8">
|
|
{table.getRowModel().rows?.length ? (
|
|
<SortableContext
|
|
items={data?.users.map((user: any) => user.id) || []}
|
|
strategy={verticalListSortingStrategy}
|
|
>
|
|
{table.getRowModel().rows.map((row) => (
|
|
<DraggableRow key={row.id} row={row} />
|
|
))}
|
|
</SortableContext>
|
|
) : (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={columns.length}
|
|
className="h-24 text-center"
|
|
>
|
|
No results.
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</DndContext>
|
|
</div>
|
|
<div className="flex items-center justify-between px-4">
|
|
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex">
|
|
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
|
{table.getFilteredRowModel().rows.length} row(s) selected.
|
|
</div>
|
|
<div className="flex w-full items-center gap-8 lg:w-fit">
|
|
<div className="hidden items-center gap-2 lg:flex">
|
|
<Label htmlFor="rows-per-page" className="text-sm font-medium">
|
|
Rows per page
|
|
</Label>
|
|
<Select
|
|
value={`${table.getState().pagination.pageSize}`}
|
|
onValueChange={(value) => {
|
|
table.setPageSize(Number(value))
|
|
}}
|
|
>
|
|
<SelectTrigger size="sm" className="w-20" id="rows-per-page">
|
|
<SelectValue
|
|
placeholder={table.getState().pagination.pageSize}
|
|
/>
|
|
</SelectTrigger>
|
|
<SelectContent side="top">
|
|
{[10, 20, 30, 40, 50].map((pageSize) => (
|
|
<SelectItem key={pageSize} value={`${pageSize}`}>
|
|
{pageSize}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex w-fit items-center justify-center text-sm font-medium">
|
|
Page {table.getState().pagination.pageIndex + 1} of{" "}
|
|
{table.getPageCount()}
|
|
</div>
|
|
<div className="ml-auto flex items-center gap-2 lg:ml-0">
|
|
<Button
|
|
variant="outline"
|
|
className="hidden h-8 w-8 p-0 lg:flex"
|
|
onClick={() => table.setPageIndex(0)}
|
|
disabled={!table.getCanPreviousPage()}
|
|
>
|
|
<span className="sr-only">Go to first page</span>
|
|
<IconChevronsLeft />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
className="size-8"
|
|
size="icon"
|
|
onClick={() => table.previousPage()}
|
|
disabled={!table.getCanPreviousPage()}
|
|
>
|
|
<span className="sr-only">Go to previous page</span>
|
|
<IconChevronLeft />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
className="size-8"
|
|
size="icon"
|
|
onClick={() => table.nextPage()}
|
|
disabled={!table.getCanNextPage()}
|
|
>
|
|
<span className="sr-only">Go to next page</span>
|
|
<IconChevronRight />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
className="hidden size-8 lg:flex"
|
|
size="icon"
|
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
|
disabled={!table.getCanNextPage()}
|
|
>
|
|
<span className="sr-only">Go to last page</span>
|
|
<IconChevronsRight />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
<TabsContent
|
|
value="past-performance"
|
|
className="flex flex-col px-4 lg:px-6"
|
|
>
|
|
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
|
</TabsContent>
|
|
<TabsContent value="key-personnel" className="flex flex-col px-4 lg:px-6">
|
|
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
|
</TabsContent>
|
|
<TabsContent
|
|
value="focus-documents"
|
|
className="flex flex-col px-4 lg:px-6"
|
|
>
|
|
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</Dialog>
|
|
)
|
|
|
|
}
|
|
|
|
const chartData = [
|
|
{ month: "January", desktop: 186, mobile: 80 },
|
|
{ month: "February", desktop: 305, mobile: 200 },
|
|
{ month: "March", desktop: 237, mobile: 120 },
|
|
{ month: "April", desktop: 73, mobile: 190 },
|
|
{ month: "May", desktop: 209, mobile: 130 },
|
|
{ month: "June", desktop: 214, mobile: 140 },
|
|
]
|
|
|
|
const chartConfig = {
|
|
desktop: {
|
|
label: "Desktop",
|
|
color: "var(--primary)",
|
|
},
|
|
mobile: {
|
|
label: "Mobile",
|
|
color: "var(--primary)",
|
|
},
|
|
} satisfies ChartConfig
|
|
|
|
function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
|
|
const isMobile = useIsMobile()
|
|
|
|
return (
|
|
<Drawer direction={isMobile ? "bottom" : "right"}>
|
|
<DrawerTrigger asChild>
|
|
<Button variant="link" className="text-foreground w-fit px-0 text-left">
|
|
{item.username}
|
|
</Button>
|
|
</DrawerTrigger>
|
|
<DrawerContent>
|
|
<DrawerHeader className="gap-1">
|
|
<DrawerTitle>{item.username}</DrawerTitle>
|
|
<DrawerDescription>
|
|
User profile and activity information
|
|
</DrawerDescription>
|
|
</DrawerHeader>
|
|
<div className="flex flex-col gap-4 overflow-y-auto px-4 text-sm">
|
|
{!isMobile && (
|
|
<>
|
|
<ChartContainer config={chartConfig}>
|
|
<AreaChart
|
|
accessibilityLayer
|
|
data={chartData}
|
|
margin={{
|
|
left: 0,
|
|
right: 10,
|
|
}}
|
|
>
|
|
<CartesianGrid vertical={false} />
|
|
<XAxis
|
|
dataKey="month"
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tickMargin={8}
|
|
tickFormatter={(value) => value.slice(0, 3)}
|
|
hide
|
|
/>
|
|
<ChartTooltip
|
|
cursor={false}
|
|
content={<ChartTooltipContent indicator="dot" />}
|
|
/>
|
|
<Area
|
|
dataKey="mobile"
|
|
type="natural"
|
|
fill="var(--color-mobile)"
|
|
fillOpacity={0.6}
|
|
stroke="var(--color-mobile)"
|
|
stackId="a"
|
|
/>
|
|
<Area
|
|
dataKey="desktop"
|
|
type="natural"
|
|
fill="var(--color-desktop)"
|
|
fillOpacity={0.4}
|
|
stroke="var(--color-desktop)"
|
|
stackId="a"
|
|
/>
|
|
</AreaChart>
|
|
</ChartContainer>
|
|
<Separator />
|
|
<div className="grid gap-2">
|
|
<div className="flex gap-2 leading-none font-medium">
|
|
Login activity up by 5.2% this month{" "}
|
|
<IconTrendingUp className="size-4" />
|
|
</div>
|
|
<div className="text-muted-foreground">
|
|
User activity and login statistics for the last 6 months. This shows
|
|
the user's engagement patterns and system usage over time.
|
|
</div>
|
|
</div>
|
|
<Separator />
|
|
</>
|
|
)}
|
|
<form className="flex flex-col gap-4">
|
|
<div className="flex flex-col gap-3">
|
|
<Label htmlFor="name">Name</Label>
|
|
<Input id="name" defaultValue={item.username} />
|
|
</div>
|
|
<div className="flex flex-col gap-3">
|
|
<Label htmlFor="email">Email</Label>
|
|
<Input id="email" defaultValue={item.email} />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="flex flex-col gap-3">
|
|
<Label htmlFor="role">Role</Label>
|
|
<Select defaultValue={item.role}>
|
|
<SelectTrigger id="role" className="w-full">
|
|
<SelectValue placeholder="Select a role" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="Administrator">Administrator</SelectItem>
|
|
<SelectItem value="Manager">Manager</SelectItem>
|
|
<SelectItem value="Developer">Developer</SelectItem>
|
|
<SelectItem value="Designer">Designer</SelectItem>
|
|
<SelectItem value="Analyst">Analyst</SelectItem>
|
|
<SelectItem value="Support">Support</SelectItem>
|
|
<SelectItem value="Guest">Guest</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<DrawerFooter>
|
|
<Button>Submit</Button>
|
|
<DrawerClose asChild>
|
|
<Button variant="outline">Done</Button>
|
|
</DrawerClose>
|
|
</DrawerFooter>
|
|
</DrawerContent>
|
|
</Drawer>
|
|
)
|
|
}
|