From 3b35c1b1ee08a78960e6595ad905f46435888085 Mon Sep 17 00:00:00 2001 From: Sanjana Singhania <147664492+sanjana-singhania@users.noreply.github.com> Date: Tue, 3 Dec 2024 20:41:20 -0500 Subject: [PATCH] Admin view (#24) * add admin auth * initial routes to fetch data * frontend wip * slight changes to frontend + add route * fetching data implemented * front end styling * fixed one eslint error * loading page * route to not found page * cleanup pagination component * merge * trying to fix types * fix types * fix tsc * make fonts bigger --------- Co-authored-by: owen --- apps/web/app/(pages)/admin/page.tsx | 14 ++ bun.lockb | Bin 280252 -> 282052 bytes .../components/src/admin/AdminDashboard.tsx | 86 +++++++++++++ packages/components/src/admin/DataTable.tsx | 90 +++++++++++++ packages/components/src/loading/Loading.tsx | 19 +++ packages/components/src/loading/Spinner.tsx | 10 ++ .../migration.sql | 16 +++ packages/db/prisma/schema.prisma | 4 +- packages/trpc/src/internal/init.ts | 10 ++ packages/trpc/src/internal/router.ts | 2 + packages/trpc/src/procedures/admin-view.ts | 12 ++ packages/trpc/src/procedures/onboarding.ts | 2 +- packages/trpc/src/utils.ts | 4 + packages/ui/package.json | 2 +- packages/ui/shad/badge.tsx | 37 ++++++ packages/ui/shad/button.tsx | 4 +- packages/ui/shad/card.tsx | 83 ++++++++++++ packages/ui/shad/table.tsx | 120 ++++++++++++++++++ packages/ui/shad/tabs.tsx | 53 ++++++++ 19 files changed, 562 insertions(+), 6 deletions(-) create mode 100644 apps/web/app/(pages)/admin/page.tsx create mode 100644 packages/components/src/admin/AdminDashboard.tsx create mode 100644 packages/components/src/admin/DataTable.tsx create mode 100644 packages/components/src/loading/Loading.tsx create mode 100644 packages/components/src/loading/Spinner.tsx create mode 100644 packages/db/prisma/migrations/20241124210934_renamed_initiator_id/migration.sql create mode 100644 packages/trpc/src/procedures/admin-view.ts create mode 100644 packages/ui/shad/badge.tsx create mode 100644 packages/ui/shad/card.tsx create mode 100644 packages/ui/shad/table.tsx create mode 100644 packages/ui/shad/tabs.tsx diff --git a/apps/web/app/(pages)/admin/page.tsx b/apps/web/app/(pages)/admin/page.tsx new file mode 100644 index 0000000..a02b538 --- /dev/null +++ b/apps/web/app/(pages)/admin/page.tsx @@ -0,0 +1,14 @@ +import AdminDashboard from "@good-dog/components/admin/AdminDashboard"; +import { HydrateClient, trpc } from "@good-dog/trpc/server"; + +export const dynamic = "force-dynamic"; + +export default async function AdminPage() { + void trpc.adminData.prefetch(); + + return ( + + + + ); +} diff --git a/bun.lockb b/bun.lockb index 5b8d3c9eced221e712793b1a531ca466e7fc6358..b2798fea80f2e06396559f5b348666b8c8862678 100755 GIT binary patch delta 34393 zcmeHwcU)A**Z%I^U1hO>N<>7(jtvEsMNtG5JH{@FMnwcc5vhuG!HQUMql4Ic?*)78 z1w}<;V!nyS5{>C`HlL zw!Wv==}PK!seXfWy3&xIkWP?8!;&HzM#d(F$0Vk(j1zcQ@S6VnK%K5U^iRTcy4sK% zhUj!2kVm1rLoN(g<#dixC5=}I7fIV3w84C$n^>QWmc!Gb$S z=yYbt4N_JF4-Zqfp#l~ZkZgDYWF^SKkZia$Bpr2$)#<7;Up71#lKHPYqIJk#pA`nL zS9GzD+F)XGctTiWgzlXLHPms@2^f?_-NHbJkhc*noVuzkWugyfyI%>~HM#o0Sg(W6_g$y(J*`w70*3mYlgqL*( zCITb(3C5kp=D~)tWO3E}db}F2@WjN3L=5{G=(LX;m6!xhw;MV~I0cf`j}03cL%%jc zcZ7auyy|zhEH`9=%Ez&DjCnT@XghTt4A^j*G)#%N<(8;YJyzBe9yMx2^w6+`$i#@z z5wS^$7-YL28CIKP$tu=?Fyjo5NQ{Y&MNjKY*6C_NH$zr|{3cD60+Q}tgXGNh!4I~6 z3_9gzDW6PL=Z|JT4Lm1sOvGr+2+V=$YB$QN(wdqK13K_fW{ij!i$Lpi17@h*ii`H`KpDv7gy@FDF=^tGW5E9H*;1PpzP=lyMDXMulT;Lq?B`PSTZ~ujWT2 z#wSN4jMY6sbQsc1@QlO`=#1d>Om&9WU7&70PLLd`O*WmbCgdm3t3qZ%=N_^Xx|Ka* zsD@xtJ(mTKKvo9-92L@V19bKv5jqhRK~szc8q z8QF46RDRf~xS<^4Gueng$IKBKY#=#Xm64+o8b*YLN41azJS1Ojh3ddl=yWU`vMl6e z@Ek%%=x;&ZUa4-gb7aHoR;iQHcePso7};L!)mRJ+oeMIYAxlbwY!u*_jz~oh9AHl;AOfs!z3hM)`E>Xv=-gc&K+-PSn%FQQVpv4VhagxW zIx#FEA#AMf7ka-g%J1dL}ArvGN>_6|UzUTbOw670!DnbB~U+S5!(PBxRwe;1PawB4#h_aPaf zkUgsY2$BwU-m5kkC}nhF!^DUroepwjSe#CG9d_)<5y%Q$|67n?=!cF9PfCc6jc9|0 zT%p&8WJJnI-8)llP;g30LPdBX>)e84Ra%>Q)F;QVjx?#}iSYJp+sI$~P zAsND$ld6O7>(p}j&^cE;PpLyO7CbvN7CPI>hh#m~Ppj>nIEDCg%)(NlMFqBi5<4|1ubvjGF>c|vGcGyR2=+~<%@f9S8 zpfL(^(mc7QR@Cvj+B4tldv?2gZ?+At<7Yh}l7C!td&Ow|vxMR^o(F#Fw(ml*@#lN| zaongsdnY;aW{3WFb<-|RfB(yjN(1zRYP&Ss-EW4nYs`Dpnb*BXtDdv1D0>0RAiYB{IX=ymDn?4R0~|6%Oe z=07KuEHT!WxhJsR`L}Hj2Les)G27bN@?!jzj6t@H0|93JV4OY?DxX+ZH~Xc!wJcY%zS-gTVQ<$^Aj-bVX3F3xz!MzZn#}zE$5)&#Ed*@wQzpy zTcGVKteJ}AQrgoNa=axI-C?9Qkb2SV079*wb^HLWYe60UI1$kpZNKB^0Hs|)))i=N z3N)Wcoeq9VM;1WqrWS+Ccc7_Nva1cS=XAE^MYeQ6LN#K$&!N5Yy<3z{*AW#c=$D^^ zRM1Hui}@L}&T8HDE#`~SY9)%z+1p|sjK~CNd8P}{+Sx+hZs{;Wtqr|sYBBeL*3({h zKD2|-TBzCti}^>X>21!9E#`(;A@m8A53`u2Knt+tP4G9LK-(3e2{9uoOk;<#1*nI zM?-rRy$_(Nt-??rZ_5}Lpg1Skav?@0*c`_Pn2#msbiVM!XmjplG5-XufxVxZ{uWbP zY_S2hQr`aNIY`mUV9Rf2F`pu@dXHhyW0&CY8#ybLwn?_!2?5HoB%9;J0CTBiHQomM zfc1gKa%k&>pFY`^J261HmTYsJ1jEtlk|=J=#8?JHQ-=cKR+f&o<-+8)Bpqy-jr|

@ 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 G<^Ka_)}7!0 delta 33399 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 diff --git a/packages/components/src/admin/AdminDashboard.tsx b/packages/components/src/admin/AdminDashboard.tsx new file mode 100644 index 0000000..254e9fe --- /dev/null +++ b/packages/components/src/admin/AdminDashboard.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { useState } from "react"; + +import { trpc } from "@good-dog/trpc/client"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@good-dog/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@good-dog/ui/tabs"; + +import { DataTable } from "./DataTable"; + +export default function AdminDashboard() { + const [activeTab, setActiveTab] = useState("users"); + const [data] = trpc.adminData.useSuspenseQuery(); + + const userData = data.users; + const groupData = data.groups; + const groupInvitesData = data.groupInvites; + + return ( +

+
+

Admin Dashboard

+ + + + Users + + + Groups + + + Invites + + +
+ + + + Users + + Manage user accounts in the system. + + + + + + + + + + + Groups + + Manage user groups and permissions. + + + + + + + + + + + Invites + + Manage pending invitations. + + + + + + + +
+
+
+
+ ); +} diff --git a/packages/components/src/admin/DataTable.tsx b/packages/components/src/admin/DataTable.tsx new file mode 100644 index 0000000..1cb6a52 --- /dev/null +++ b/packages/components/src/admin/DataTable.tsx @@ -0,0 +1,90 @@ +"use client"; + +import React from "react"; + +import type { GetProcedureOutput } from "@good-dog/trpc/utils"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@good-dog/ui/table"; + +export type AdminDataTypes = { + [T in keyof GetProcedureOutput<"adminData">]: GetProcedureOutput<"adminData">[T][number]; +}; + +interface DataColumn { + accessorKey: keyof AdminDataTypes[T] & string; + header: string; + cell?: ( + value: AdminDataTypes[T][keyof AdminDataTypes[T] & string], + ) => React.ReactNode; +} + +const columns: { [T in keyof AdminDataTypes]: DataColumn[] } = { + users: [ + { accessorKey: "firstName", header: "First Name" }, + { accessorKey: "lastName", header: "Last Name" }, + { accessorKey: "email", header: "Email" }, + { accessorKey: "role", header: "Role" }, + { accessorKey: "stageName", header: "Stage Name" }, + { accessorKey: "isSongWriter", header: "Songwriter?" }, + { accessorKey: "isAscapAffiliated", header: "ASCAP Affiliated?" }, + { accessorKey: "isBmiAffiliated", header: "BMI Affiliated?" }, + { accessorKey: "createdAt", header: "Date of Creation" }, + { accessorKey: "updatedAt", header: "Date Last Updated" }, + ], + groups: [ + { accessorKey: "name", header: "Name" }, + { accessorKey: "createdAt", header: "Date of Creation" }, + { accessorKey: "updatedAt", header: "Date Last Updated" }, + ], + groupInvites: [ + { accessorKey: "email", header: "Email" }, + { accessorKey: "firstName", header: "First Name" }, + { accessorKey: "lastName", header: "Last Name" }, + { accessorKey: "stageName", header: "Stage Name" }, + { accessorKey: "role", header: "Role" }, + { accessorKey: "isSongWriter", header: "Songwriter?" }, + { accessorKey: "isAscapAffiliated", header: "ASCAP Affiliated?" }, + { accessorKey: "isBmiAffiliated", header: "BMI Affiliated?" }, + { accessorKey: "createdAt", header: "Date of Creation" }, + ], +}; + +interface DataTableProps { + table: T; + data: AdminDataTypes[T][]; +} + +export function DataTable({ + table, + data, +}: DataTableProps) { + return ( + + + + {columns[table].map((column) => ( + {column.header} + ))} + + + + {data.map((entry, idx) => ( + + {columns[table].map((column) => ( + + {column.cell?.(entry[column.accessorKey]) ?? + String(entry[column.accessorKey])} + + ))} + + ))} + +
+ ); +} diff --git a/packages/components/src/loading/Loading.tsx b/packages/components/src/loading/Loading.tsx new file mode 100644 index 0000000..47fe221 --- /dev/null +++ b/packages/components/src/loading/Loading.tsx @@ -0,0 +1,19 @@ +import { Spinner } from "./Spinner"; + +export default function LoadingPage() { + return ( +
+
+

+ LOADING +

+
+ +
+

+ Please wait while we fetch your content. +

+
+
+ ); +} diff --git a/packages/components/src/loading/Spinner.tsx b/packages/components/src/loading/Spinner.tsx new file mode 100644 index 0000000..f826eec --- /dev/null +++ b/packages/components/src/loading/Spinner.tsx @@ -0,0 +1,10 @@ +export function Spinner({ className = "" }: { className?: string }) { + return ( +
+ Loading... +
+ ); +} diff --git a/packages/db/prisma/migrations/20241124210934_renamed_initiator_id/migration.sql b/packages/db/prisma/migrations/20241124210934_renamed_initiator_id/migration.sql new file mode 100644 index 0000000..e724753 --- /dev/null +++ b/packages/db/prisma/migrations/20241124210934_renamed_initiator_id/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - You are about to drop the column `intitiatorId` on the `GroupInvite` table. All the data in the column will be lost. + - Added the required column `initiatorId` to the `GroupInvite` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "GroupInvite" DROP CONSTRAINT "GroupInvite_intitiatorId_fkey"; + +-- AlterTable +ALTER TABLE "GroupInvite" DROP COLUMN "intitiatorId", +ADD COLUMN "initiatorId" TEXT NOT NULL; + +-- AddForeignKey +ALTER TABLE "GroupInvite" ADD CONSTRAINT "GroupInvite_initiatorId_fkey" FOREIGN KEY ("initiatorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 82f33f1..9c49790 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -61,7 +61,7 @@ model Group { model GroupInvite { inviteId String @default(cuid()) @map("id") groupId String - intitiatorId String + initiatorId String email String firstName String lastName String @@ -71,7 +71,7 @@ model GroupInvite { isAscapAffiliated Boolean @default(false) isBmiAffiliated Boolean @default(false) group Group @relation(fields: [groupId], references: [groupId], onDelete: Cascade) - intitiator User @relation(fields: [intitiatorId], references: [userId], onDelete: Cascade) + intitiator User @relation(fields: [initiatorId], references: [userId], onDelete: Cascade) createdAt DateTime @default(now()) @@unique([groupId, email]) diff --git a/packages/trpc/src/internal/init.ts b/packages/trpc/src/internal/init.ts index 2332a1f..70fb011 100644 --- a/packages/trpc/src/internal/init.ts +++ b/packages/trpc/src/internal/init.ts @@ -50,6 +50,7 @@ export const createCallerFactory = t.createCallerFactory; // Procedure builders export const baseProcedureBuilder = t.procedure; + export const authenticatedProcedureBuilder = baseProcedureBuilder.use( async ({ ctx, next }) => { const sessionId = getSessionCookie(); @@ -89,6 +90,15 @@ export const authenticatedProcedureBuilder = baseProcedureBuilder.use( }, ); +export const adminAuthenticatedProcedureBuilder = + authenticatedProcedureBuilder.use(async ({ ctx, next }) => { + if (ctx.session.user.role !== "ADMIN") { + throw new TRPCError({ code: "FORBIDDEN" }); + } + + return next({ ctx }); + }); + // This middleware is used to prevent authenticated users from accessing a resource export const notAuthenticatedProcedureBuilder = baseProcedureBuilder.use( async ({ ctx, next }) => { diff --git a/packages/trpc/src/internal/router.ts b/packages/trpc/src/internal/router.ts index 7cc8937..dbf0baf 100644 --- a/packages/trpc/src/internal/router.ts +++ b/packages/trpc/src/internal/router.ts @@ -1,3 +1,4 @@ +import { getAdminViewProcedure } from "../procedures/admin-view"; import { deleteAccountProcedure, signInProcedure, @@ -31,6 +32,7 @@ export const appRouter = createTRPCRouter({ user: getUserProcedure, sendForgotPasswordEmail: sendForgotPasswordEmailProcedure, confirmPasswordReset: confirmPasswordResetProcedure, + adminData: getAdminViewProcedure, }); export type AppRouter = typeof appRouter; diff --git a/packages/trpc/src/procedures/admin-view.ts b/packages/trpc/src/procedures/admin-view.ts new file mode 100644 index 0000000..1f85851 --- /dev/null +++ b/packages/trpc/src/procedures/admin-view.ts @@ -0,0 +1,12 @@ +import { adminAuthenticatedProcedureBuilder } from "../internal/init"; + +export const getAdminViewProcedure = adminAuthenticatedProcedureBuilder.query( + async ({ ctx }) => { + const [users, groups, groupInvites] = await Promise.all([ + ctx.prisma.user.findMany({ omit: { hashedPassword: true } }), + ctx.prisma.group.findMany(), + ctx.prisma.groupInvite.findMany(), + ]); + return { users, groups, groupInvites }; + }, +); diff --git a/packages/trpc/src/procedures/onboarding.ts b/packages/trpc/src/procedures/onboarding.ts index 107ceac..bbd3bdd 100644 --- a/packages/trpc/src/procedures/onboarding.ts +++ b/packages/trpc/src/procedures/onboarding.ts @@ -77,7 +77,7 @@ export const onboardingProcedure = authenticatedProcedureBuilder createMany: { data: input.groupMembers?.map((member) => ({ - intitiatorId: ctx.session.userId, + initiatorId: ctx.session.userId, email: member.email, firstName: member.firstName, lastName: member.lastName, diff --git a/packages/trpc/src/utils.ts b/packages/trpc/src/utils.ts index 2a0e4d3..73b9308 100644 --- a/packages/trpc/src/utils.ts +++ b/packages/trpc/src/utils.ts @@ -1,4 +1,5 @@ import type { TRPCClientErrorLike } from "@trpc/client"; +import type { inferProcedureOutput } from "@trpc/server"; import { z } from "zod"; import type { AppRouter } from "./internal/router"; @@ -13,3 +14,6 @@ export const zPreProcessEmptyString = (schema: I) => return arg; } }, schema); + +export type GetProcedureOutput = + inferProcedureOutput; diff --git a/packages/ui/package.json b/packages/ui/package.json index ca3ac67..920809e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -34,12 +34,12 @@ "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-avatar": "^1.1.0", - "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", + "@radix-ui/react-tabs": "^1.1.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "date-fns": "^4.1.0", diff --git a/packages/ui/shad/badge.tsx b/packages/ui/shad/badge.tsx new file mode 100644 index 0000000..a99527d --- /dev/null +++ b/packages/ui/shad/badge.tsx @@ -0,0 +1,37 @@ +import type { VariantProps } from "class-variance-authority"; +import React from "react"; +import { cva } from "class-variance-authority"; + +import { cn } from "@good-dog/ui"; + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ); +} + +export { Badge, badgeVariants }; diff --git a/packages/ui/shad/button.tsx b/packages/ui/shad/button.tsx index 4d8fc8d..425a0bb 100644 --- a/packages/ui/shad/button.tsx +++ b/packages/ui/shad/button.tsx @@ -3,10 +3,10 @@ import React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cva } from "class-variance-authority"; -import { cn } from "."; +import { cn } from "@good-dog/ui"; const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { diff --git a/packages/ui/shad/card.tsx b/packages/ui/shad/card.tsx new file mode 100644 index 0000000..3469abf --- /dev/null +++ b/packages/ui/shad/card.tsx @@ -0,0 +1,83 @@ +import React from "react"; + +import { cn } from "@good-dog/ui"; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/packages/ui/shad/table.tsx b/packages/ui/shad/table.tsx new file mode 100644 index 0000000..fdb94ff --- /dev/null +++ b/packages/ui/shad/table.tsx @@ -0,0 +1,120 @@ +import React from "react"; + +import { cn } from "@good-dog/ui"; + +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) => ( +
[role=checkbox]]:translate-y-[2px]", + className, + )} + {...props} + /> +)); +TableHead.displayName = "TableHead"; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className, + )} + {...props} + /> +)); +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/packages/ui/shad/tabs.tsx b/packages/ui/shad/tabs.tsx new file mode 100644 index 0000000..2f1bedb --- /dev/null +++ b/packages/ui/shad/tabs.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; + +import { cn } from "@good-dog/ui"; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent };