diff --git a/package.json b/package.json index e13f001..ef9a8c4 100644 --- a/package.json +++ b/package.json @@ -21,16 +21,19 @@ "@lucia-auth/adapter-drizzle": "1.0.0", "@planetscale/database": "^1.11.0", "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", "@react-email/components": "^0.0.12", "@react-email/render": "^0.0.10", "@t3-oss/env-nextjs": "^0.7.1", "@tanstack/react-query": "^4.36.1", + "@tanstack/react-table": "^8.12.0", "@trpc/client": "^10.43.6", "@trpc/next": "^10.43.6", "@trpc/react-query": "^10.43.6", @@ -41,6 +44,7 @@ "drizzle-orm": "^0.29.3", "json-to-zod": "^1.1.2", "lucia": "3.0.0", + "lucide-react": "^0.335.0", "next": "^14.1.0", "next-themes": "^0.2.1", "nodemailer": "^6.9.7", diff --git a/src/app/(main)/form/[id]/page.tsx b/src/app/(main)/form/[id]/page.tsx index 9a881c8..887af60 100644 --- a/src/app/(main)/form/[id]/page.tsx +++ b/src/app/(main)/form/[id]/page.tsx @@ -1,18 +1,42 @@ import { api } from "~/trpc/server"; +import { Copy } from "lucide-react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { SubmissionsTable } from "./submissions-table"; export default async function FormPage({ params }: { params: { id: string } }) { - const formSubmissions = await api.formData.all.query({ formId: params.id }); + const formId = params.id; + const [form, formSubmissions] = await Promise.all([ + api.form.get.query({ formId }), + api.formData.all.query({ formId }), + ]); return ( -
-
My Form Id: {params.id}
-
My Submittions
- +
- {formSubmissions.map((f) => { - return
{JSON.stringify(f.data as string)}
; - })} +

{form?.title}

+ +
+ + {formId} + + +
+ + + + Submissions + Setup + Analytics + Settings + + + + + Change your password here. + Look at your analytics here + Edit your form here +
); } diff --git a/src/app/(main)/form/[id]/submissions-table.tsx b/src/app/(main)/form/[id]/submissions-table.tsx new file mode 100644 index 0000000..9d5b359 --- /dev/null +++ b/src/app/(main)/form/[id]/submissions-table.tsx @@ -0,0 +1,228 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +"use client"; + +import * as React from "react"; +import { DotsHorizontalIcon } from "@radix-ui/react-icons"; +import { + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import type { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, +} from "@tanstack/react-table"; + +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import type { FormData } from "@/server/db/schema"; + +type SubmissionsTableProps = { + submissions: FormData[]; +}; + +export function SubmissionsTable({ submissions }: SubmissionsTableProps) { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => { + console.log(table.getIsAllPageRowsSelected()); + + return ( + + table.toggleAllPageRowsSelected(!!value) + } + aria-label="Select all" + /> + ); + }, + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + + ...Object.keys(submissions[0]?.data as object).map((submission: any) => { + return { + accessorKey: submission, + header: () => { + return ( + + ); + }, + cell: ({ row }: any) => ( +
{row.getValue(submission)}
+ ), + }; + }), + + { + id: "actions", + enableHiding: false, + cell: ({ row }) => { + return ( + + + + + + Actions + Copy submission ID + + View customer + View payment details + + + ); + }, + }, + ]; + + const table = useReactTable({ + data: submissions.map((submission: any) => submission.data), + columns: columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + return ( +
+
+ + + {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. + + + )} + +
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+ + +
+
+
+ ); +} diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..ad4ddbc --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "~/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..1a477d6 --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,31 @@ +"use client"; + +import * as React from "react"; +import * as PopoverPrimitive from "@radix-ui/react-popover"; + +import { cn } from "~/lib/utils"; + +const Popover = PopoverPrimitive.Root; + +const PopoverTrigger = PopoverPrimitive.Trigger; + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverTrigger, PopoverContent }; diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx new file mode 100644 index 0000000..b3b3711 --- /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, +} diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 6773815..2b83c87 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -155,3 +155,9 @@ export const postRelations = relations(posts, ({ one }) => ({ export type Post = typeof posts.$inferSelect; export type NewPost = typeof posts.$inferInsert; + +export type FormData = typeof formDatas.$inferSelect; +export type NewFormData = typeof formDatas.$inferInsert; + +export type Form = typeof forms.$inferSelect; +export type NewForm = typeof forms.$inferInsert; diff --git a/tailwind.config.ts b/tailwind.config.ts index 35b3a19..65781ed 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -9,7 +9,7 @@ export default { center: true, padding: "2rem", screens: { - lg: "768px", + lg: "1000px", }, }, extend: {