Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add submissions table #10

Merged
merged 2 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
40 changes: 32 additions & 8 deletions src/app/(main)/form/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<div>My Form Id: {params.id}</div>
<div>My Submittions</div>

<div className="space-y-6">
<div>
{formSubmissions.map((f) => {
return <div key={f.id}>{JSON.stringify(f.data as string)}</div>;
})}
<h1 className="text-3xl font-medium">{form?.title}</h1>

<div className="mt-2 flex items-center gap-2">
<span className="inline-flex items-center rounded-lg bg-muted px-2 py-0.5 text-sm font-medium">
{formId}
</span>
<Copy className="h-4 w-4 text-muted-foreground" />
</div>
</div>

<Tabs defaultValue="submissions">
<TabsList>
<TabsTrigger value="submissions">Submissions</TabsTrigger>
<TabsTrigger value="setup">Setup</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="submissions" className="my-6">
<SubmissionsTable submissions={formSubmissions} />
</TabsContent>
<TabsContent value="setup">Change your password here.</TabsContent>
<TabsContent value="analytics">Look at your analytics here</TabsContent>
<TabsContent value="settings">Edit your form here</TabsContent>
</Tabs>
</div>
);
}
228 changes: 228 additions & 0 deletions src/app/(main)/form/[id]/submissions-table.tsx
Original file line number Diff line number Diff line change
@@ -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<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[],
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});

const columns: ColumnDef<FormData["data"]>[] = [
{
id: "select",
header: ({ table }) => {
console.log(table.getIsAllPageRowsSelected());

return (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
aria-label="Select all"
/>
);
},
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value: any) => 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 (
<Button variant="ghost" className="px-0 py-0 capitalize">
{submission}
{/* <CaretSortIcon className="ml-2 h-4 w-4" /> */}
</Button>
);
},
cell: ({ row }: any) => (
<div className="lowercase">{row.getValue(submission)}</div>
),
};
}),

{
id: "actions",
enableHiding: false,
cell: ({ row }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<DotsHorizontalIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Copy submission ID</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];

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 (
<div className="mt-6 w-full">
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
);
}
30 changes: 30 additions & 0 deletions src/components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName

export { Checkbox }
31 changes: 31 additions & 0 deletions src/components/ui/popover.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;

export { Popover, PopoverTrigger, PopoverContent };
Loading
Loading