diff --git a/package.json b/package.json index 25b5fa7..2f3113e 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@radix-ui/react-toast": "1.2.2", "@react-email/components": "0.0.25", "@react-email/tailwind": "0.1.0", + "@tanstack/react-table": "8.20.5", "axios": "1.7.7", "better-sqlite3": "11.3.0", "class-variance-authority": "0.7.0", diff --git a/src/app/[orgId]/layout.tsx b/src/app/[orgId]/layout.tsx index 3f1312e..c8016fd 100644 --- a/src/app/[orgId]/layout.tsx +++ b/src/app/[orgId]/layout.tsx @@ -53,7 +53,7 @@ export default async function ConfigurationLaytout({ return ( <> -
+
diff --git a/src/app/[orgId]/sites/[siteId]/layout.tsx b/src/app/[orgId]/sites/[siteId]/layout.tsx index 973323c..01863e0 100644 --- a/src/app/[orgId]/sites/[siteId]/layout.tsx +++ b/src/app/[orgId]/sites/[siteId]/layout.tsx @@ -1,19 +1,21 @@ -import { Metadata } from "next" -import Image from "next/image" +import { Metadata } from "next"; +import Image from "next/image"; -import { Separator } from "@/components/ui/separator" -import { SidebarNav } from "@/components/sidebar-nav" -import SiteProvider from "@app/providers/SiteProvider" -import { internal } from "@app/api" -import { GetSiteResponse } from "@server/routers/site" -import { AxiosResponse } from "axios" -import { redirect } from "next/navigation" -import { authCookieHeader } from "@app/api/cookies" +import { Separator } from "@/components/ui/separator"; +import { SidebarNav } from "@/components/sidebar-nav"; +import SiteProvider from "@app/providers/SiteProvider"; +import { internal } from "@app/api"; +import { GetSiteResponse } from "@server/routers/site"; +import { AxiosResponse } from "axios"; +import { redirect } from "next/navigation"; +import { authCookieHeader } from "@app/api/cookies"; +import Link from "next/link"; +import { ArrowLeft, ChevronLeft } from "lucide-react"; export const metadata: Metadata = { title: "Forms", description: "Advanced form example using react-hook-form and Zod.", -} +}; const sidebarNavItems = [ { @@ -32,21 +34,27 @@ const sidebarNavItems = [ title: "Display", href: "/{orgId}/sites/{siteId}/display", }, -] +]; interface SettingsLayoutProps { - children: React.ReactNode, - params: { siteId: string, orgId: string } + children: React.ReactNode; + params: { siteId: string; orgId: string }; } -export default async function SettingsLayout({ children, params }: SettingsLayoutProps) { +export default async function SettingsLayout({ + children, + params, +}: SettingsLayoutProps) { let site = null; if (params.siteId !== "create") { try { - const res = await internal.get>(`/site/${params.siteId}`, authCookieHeader()); + const res = await internal.get>( + `/site/${params.siteId}`, + authCookieHeader(), + ); site = res.data.data; } catch { - redirect(`/${params.orgId}/sites`) + redirect(`/${params.orgId}/sites`); } } @@ -68,25 +76,47 @@ export default async function SettingsLayout({ children, params }: SettingsLayou className="hidden dark:block" />
+ +
+ +
+ + View all sites +
+ +
+
-

Settings

+

+ {params.siteId == "create" + ? "New Site" + : site?.name + " Settings" || "Site Settings" + } +

- {params.siteId == "create" ? "Create site..." : "Manage settings on " + site?.name || ""}. + {params.siteId == "create" + ? "Create a new site" + : "Configure the settings on your site: " + + site?.name || ""} + .

- -
+
- - {children} - + {children}
- ) + ); } diff --git a/src/app/[orgId]/sites/components/DataTable.tsx b/src/app/[orgId]/sites/components/DataTable.tsx new file mode 100644 index 0000000..a797df0 --- /dev/null +++ b/src/app/[orgId]/sites/components/DataTable.tsx @@ -0,0 +1,140 @@ +"use client"; + +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, + getPaginationRowModel, + SortingState, + getSortedRowModel, + ColumnFiltersState, + getFilteredRowModel, +} from "@tanstack/react-table"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Button } from "@app/components/ui/button"; +import { useState } from "react"; +import { Input } from "@app/components/ui/input"; +import { DataTablePagination } from "./DataTablePagination"; +import { Plus } from "lucide-react"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + addSite?: () => void; +} + +export function DataTable({ + addSite, + columns, + data, +}: DataTableProps) { + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + state: { + sorting, + columnFilters, + }, + }); + + return ( +
+
+ + table + .getColumn("name") + ?.setFilterValue(event.target.value) + } + className="max-w-sm" + /> + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+ +
+
+ ); +} diff --git a/src/app/[orgId]/sites/components/DataTablePagination.tsx b/src/app/[orgId]/sites/components/DataTablePagination.tsx new file mode 100644 index 0000000..ac481a8 --- /dev/null +++ b/src/app/[orgId]/sites/components/DataTablePagination.tsx @@ -0,0 +1,102 @@ +import { + ChevronLeftIcon, + ChevronRightIcon, + DoubleArrowLeftIcon, + DoubleArrowRightIcon, +} from "@radix-ui/react-icons"; +import { Table } from "@tanstack/react-table"; + +import { Button } from "@app/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@app/components/ui/select"; + +interface DataTablePaginationProps { + table: Table; +} + +export function DataTablePagination({ + table, +}: DataTablePaginationProps) { + return ( +
+
+
+

Rows per page

+ +
+
+ Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} +
+
+ + + + +
+
+
+ ); +} diff --git a/src/app/[orgId]/sites/components/SitesTable.tsx b/src/app/[orgId]/sites/components/SitesTable.tsx new file mode 100644 index 0000000..4fadab8 --- /dev/null +++ b/src/app/[orgId]/sites/components/SitesTable.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { DataTable } from "./DataTable"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@app/components/ui/dropdown-menu"; +import { Button } from "@app/components/ui/button"; +import { ArrowUpDown, MoreHorizontal } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; + +export type SiteRow = { + id: string; + name: string; + mbIn: number; + mbOut: number; + orgId: string; +}; + +export const columns: ColumnDef[] = [ + { + accessorKey: "id", + header: ({ column }) => { + return ( + + ); + }, + }, + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ); + }, + }, + { + accessorKey: "mbIn", + header: "MB In", + }, + { + accessorKey: "mbOut", + header: "MB Out", + }, + { + id: "actions", + cell: ({ row }) => { + const siteRow = row.original; + + return ( + + + + + + + + View settings + + + + + ); + }, + }, +]; + +type SitesTableProps = { + sites: SiteRow[]; + orgId: string; +}; + +export default function SitesTable({ sites, orgId }: SitesTableProps) { + const router = useRouter(); + + return ( + { + router.push(`/${orgId}/sites/create`); + }} + /> + ); +} diff --git a/src/app/[orgId]/sites/page.tsx b/src/app/[orgId]/sites/page.tsx index 15b8677..86190f0 100644 --- a/src/app/[orgId]/sites/page.tsx +++ b/src/app/[orgId]/sites/page.tsx @@ -2,6 +2,7 @@ import { internal } from "@app/api"; import { authCookieHeader } from "@app/api/cookies"; import { ListSitesResponse } from "@server/routers/site"; import { AxiosResponse } from "axios"; +import SitesTable, { SiteRow } from "./components/SitesTable"; type SitesPageProps = { params: { orgId: string }; @@ -19,9 +20,19 @@ export default async function Page({ params }: SitesPageProps) { console.error("Error fetching sites", e); } + const siteRows: SiteRow[] = sites.map((site) => { + return { + id: site.siteId.toString(), + name: site.name, + mbIn: site.megabytesIn || 0, + mbOut: site.megabytesOut || 0, + orgId: params.orgId, + }; + }); + return ( <> -
+

Manage Sites

@@ -29,6 +40,8 @@ export default async function Page({ params }: SitesPageProps) { Manage your existing sites here or create a new one.

+ + ); } diff --git a/src/app/globals.css b/src/app/globals.css index e8e65eb..bc8a229 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -36,7 +36,7 @@ --primary-foreground: 0 0% 100%; --secondary: 231 10% 20%; --secondary-foreground: 0 0% 100%; - --muted: 193 10% 25%; + --muted: 231 10% 18%; --muted-foreground: 231 0% 65%; --accent: 193 10% 25%; --accent-foreground: 231 0% 95%; diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx new file mode 100644 index 0000000..7f3502f --- /dev/null +++ b/src/components/ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}