From 9dc033b2a078dfd3c535026ce3d6c23a6ee1d443 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Sat, 11 Oct 2025 00:29:04 -0700 Subject: [PATCH] feat: add commissioner model and league management features --- app/components/ui/alert-dialog.tsx | 157 +++++++++++ app/components/ui/sonner.tsx | 21 ++ app/models/commissioner.ts | 71 +++++ app/models/index.ts | 1 + app/root.tsx | 2 + app/routes.ts | 1 + app/routes/home.tsx | 13 + app/routes/leagues/$leagueId.settings.tsx | 260 ++++++++++++++++++ app/routes/leagues/$leagueId.tsx | 55 +++- app/routes/leagues/new.tsx | 7 + database/schema.ts | 9 + drizzle/0002_tired_iron_man.sql | 12 + drizzle/meta/0002_snapshot.json | 313 ++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + package-lock.json | 51 ++++ package.json | 3 + 16 files changed, 979 insertions(+), 4 deletions(-) create mode 100644 app/components/ui/alert-dialog.tsx create mode 100644 app/components/ui/sonner.tsx create mode 100644 app/models/commissioner.ts create mode 100644 app/routes/leagues/$leagueId.settings.tsx create mode 100644 drizzle/0002_tired_iron_man.sql create mode 100644 drizzle/meta/0002_snapshot.json diff --git a/app/components/ui/alert-dialog.tsx b/app/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..43d2652 --- /dev/null +++ b/app/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "app/lib/utils" +import { buttonVariants } from "app/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/app/components/ui/sonner.tsx b/app/components/ui/sonner.tsx new file mode 100644 index 0000000..7ee6134 --- /dev/null +++ b/app/components/ui/sonner.tsx @@ -0,0 +1,21 @@ +import { Toaster as Sonner } from "sonner" + +type ToasterProps = React.ComponentProps + +const Toaster = ({ ...props }: ToasterProps) => { + return ( + + ) +} + +export { Toaster } diff --git a/app/models/commissioner.ts b/app/models/commissioner.ts new file mode 100644 index 0000000..6d7a55b --- /dev/null +++ b/app/models/commissioner.ts @@ -0,0 +1,71 @@ +import { eq, and } from "drizzle-orm"; +import { database } from "~/database/context"; +import * as schema from "~/database/schema"; + +export type Commissioner = typeof schema.commissioners.$inferSelect; +export type NewCommissioner = typeof schema.commissioners.$inferInsert; + +export async function createCommissioner( + data: NewCommissioner +): Promise { + const db = database(); + const [commissioner] = await db + .insert(schema.commissioners) + .values(data) + .returning(); + return commissioner; +} + +export async function findCommissionersByLeagueId( + leagueId: string +): Promise { + const db = database(); + return await db.query.commissioners.findMany({ + where: eq(schema.commissioners.leagueId, leagueId), + orderBy: (commissioners, { asc }) => [asc(commissioners.createdAt)], + }); +} + +export async function findCommissionersByUserId( + userId: string +): Promise { + const db = database(); + return await db.query.commissioners.findMany({ + where: eq(schema.commissioners.userId, userId), + orderBy: (commissioners, { desc }) => [desc(commissioners.createdAt)], + }); +} + +export async function isCommissioner( + leagueId: string, + userId: string +): Promise { + const db = database(); + const commissioner = await db.query.commissioners.findFirst({ + where: and( + eq(schema.commissioners.leagueId, leagueId), + eq(schema.commissioners.userId, userId) + ), + }); + return !!commissioner; +} + +export async function deleteCommissioner(id: string): Promise { + const db = database(); + await db.delete(schema.commissioners).where(eq(schema.commissioners.id, id)); +} + +export async function removeCommissionerByLeagueAndUser( + leagueId: string, + userId: string +): Promise { + const db = database(); + await db + .delete(schema.commissioners) + .where( + and( + eq(schema.commissioners.leagueId, leagueId), + eq(schema.commissioners.userId, userId) + ) + ); +} diff --git a/app/models/index.ts b/app/models/index.ts index f44153d..cf1405e 100644 --- a/app/models/index.ts +++ b/app/models/index.ts @@ -2,3 +2,4 @@ export * from "./league"; export * from "./season"; export * from "./team"; +export * from "./commissioner"; diff --git a/app/root.tsx b/app/root.tsx index 0a137cc..1359d94 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -12,6 +12,7 @@ import "./app.css"; import { rootAuthLoader } from "@clerk/react-router/server"; import { Navbar } from "~/components/navbar"; import { ClerkProvider } from "@clerk/react-router"; +import { Toaster } from "~/components/ui/sonner"; export async function loader(args: Route.LoaderArgs) { return rootAuthLoader(args); @@ -55,6 +56,7 @@ export default function App({ loaderData }: Route.ComponentProps) {
+ ); } diff --git a/app/routes.ts b/app/routes.ts index e60ba89..4bbd149 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -4,4 +4,5 @@ export default [ index("routes/home.tsx"), route("leagues/new", "routes/leagues/new.tsx"), route("leagues/:leagueId", "routes/leagues/$leagueId.tsx"), + route("leagues/:leagueId/settings", "routes/leagues/$leagueId.settings.tsx"), ] satisfies RouteConfig; diff --git a/app/routes/home.tsx b/app/routes/home.tsx index 0ab1e93..ba3695f 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -1,5 +1,8 @@ +import { useEffect } from "react"; +import { useSearchParams } from "react-router"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; +import { toast } from "sonner"; import type { Route } from "./+types/home"; import { Welcome } from "../welcome/welcome"; @@ -50,6 +53,16 @@ export async function loader({ context }: Route.LoaderArgs) { } export default function Home({ actionData, loaderData }: Route.ComponentProps) { + const [searchParams, setSearchParams] = useSearchParams(); + + useEffect(() => { + if (searchParams.get("deleted") === "true") { + toast.success("League deleted successfully"); + // Remove the query parameter + setSearchParams({}); + } + }, [searchParams, setSearchParams]); + return ( 50) { + return { error: "League name must be between 3 and 50 characters" }; + } + + try { + await updateLeague(leagueId, { + name: name.trim(), + }); + + return { success: "League updated successfully" }; + } catch (error) { + console.error("Error updating league:", error); + return { error: "Failed to update league. Please try again." }; + } + } + + return { error: "Invalid action" }; +} + +export default function LeagueSettings({ loaderData, actionData }: Route.ComponentProps) { + const { league, teamCount } = loaderData; + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + + const handleDelete = () => { + setIsDeleteDialogOpen(false); + // The form submission will handle the actual deletion + }; + + return ( +
+
+
+

League Settings

+ +
+

{league.name}

+
+ +
+ {/* Update League Form */} + + + General Settings + + Update your league's basic information + + + +
+ + +
+ + +
+ +
+ + +

+ Team count cannot be changed after league creation +

+
+ + {actionData?.success && ( +
+ {actionData.success} +
+ )} + + {actionData?.error && ( +
+ {actionData.error} +
+ )} + + +
+
+
+ + {/* Danger Zone */} + + + Danger Zone + + Irreversible and destructive actions + + + + + + + + + + Are you absolutely sure? + + This will permanently delete {league.name} and all + associated data including seasons, teams, and commissioners. This + action cannot be undone. + + + + Cancel +
+ + + Delete League + +
+
+
+
+
+
+
+
+ ); +} diff --git a/app/routes/leagues/$leagueId.tsx b/app/routes/leagues/$leagueId.tsx index 38f4786..0bc5dea 100644 --- a/app/routes/leagues/$leagueId.tsx +++ b/app/routes/leagues/$leagueId.tsx @@ -3,6 +3,7 @@ import { getAuth } from "@clerk/react-router/server"; import { findLeagueById } from "~/models/league"; import { findCurrentSeason } from "~/models/season"; import { findTeamsBySeasonId } from "~/models/team"; +import { findCommissionersByLeagueId } from "~/models/commissioner"; import type { Route } from "./+types/$leagueId"; import { Card, @@ -31,25 +32,40 @@ export async function loader(args: Route.LoaderArgs) { // Fetch teams for current season const teams = season ? await findTeamsBySeasonId(season.id) : []; + // Fetch commissioners + const commissioners = await findCommissionersByLeagueId(leagueId); + + // Check if current user is a commissioner + const isUserCommissioner = commissioners.some(c => c.userId === userId); + return { league, season, teams, + commissioners, currentUserId: userId, + isUserCommissioner, }; } export default function LeagueHome({ loaderData }: Route.ComponentProps) { - const { league, season, teams, currentUserId } = loaderData; + const { league, season, teams, commissioners, currentUserId, isUserCommissioner } = loaderData; return (

{league.name}

- +
+ {isUserCommissioner && ( + + )} + +
{season && (

@@ -124,6 +140,37 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) { )} + + + + Commissioners + + {commissioners.length} commissioner{commissioners.length !== 1 ? "s" : ""} + + + +

+ {commissioners.map((commissioner, index) => ( +
+
+

+ Commissioner {index + 1} + {commissioner.userId === currentUserId && ( + (You) + )} +

+

+ Since {new Date(commissioner.createdAt).toLocaleDateString()} +

+
+
+ ))} +
+ +
); diff --git a/app/routes/leagues/new.tsx b/app/routes/leagues/new.tsx index 61abb55..2db15e4 100644 --- a/app/routes/leagues/new.tsx +++ b/app/routes/leagues/new.tsx @@ -3,6 +3,7 @@ import { getAuth } from "@clerk/react-router/server"; import { createLeague } from "~/models/league"; import { createSeason } from "~/models/season"; import { createManyTeams } from "~/models/team"; +import { createCommissioner } from "~/models/commissioner"; import type { Route } from "./+types/new"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; @@ -55,6 +56,12 @@ export async function action(args: Route.ActionArgs) { createdBy: userId, }); + // Make creator a commissioner + await createCommissioner({ + leagueId: league.id, + userId: userId, + }); + // Create first season const currentYear = new Date().getFullYear(); const season = await createSeason({ diff --git a/database/schema.ts b/database/schema.ts index 2e57342..e469138 100644 --- a/database/schema.ts +++ b/database/schema.ts @@ -43,3 +43,12 @@ export const teams = pgTable("teams", { createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); + +export const commissioners = pgTable("commissioners", { + id: uuid("id").primaryKey().defaultRandom(), + leagueId: uuid("league_id") + .notNull() + .references(() => leagues.id, { onDelete: "cascade" }), + userId: varchar("user_id", { length: 255 }).notNull(), // Clerk user ID + createdAt: timestamp("created_at").defaultNow().notNull(), +}); diff --git a/drizzle/0002_tired_iron_man.sql b/drizzle/0002_tired_iron_man.sql new file mode 100644 index 0000000..ff9b2fa --- /dev/null +++ b/drizzle/0002_tired_iron_man.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS "commissioners" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "league_id" uuid NOT NULL, + "user_id" varchar(255) NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "commissioners" ADD CONSTRAINT "commissioners_league_id_leagues_id_fk" FOREIGN KEY ("league_id") REFERENCES "public"."leagues"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..e6fe979 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,313 @@ +{ + "id": "34297d0b-9216-46c7-9d1d-5b81f1402786", + "prevId": "dfca1306-7467-4d4e-8b9f-546611ed8b00", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.commissioners": { + "name": "commissioners", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "commissioners_league_id_leagues_id_fk": { + "name": "commissioners_league_id_leagues_id_fk", + "tableFrom": "commissioners", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.guestBook": { + "name": "guestBook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "guestBook_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "guestBook_email_unique": { + "name": "guestBook_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.leagues": { + "name": "leagues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seasons": { + "name": "seasons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "season_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pre_draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "seasons_league_id_leagues_id_fk": { + "name": "seasons_league_id_leagues_id_fk", + "tableFrom": "seasons", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "teams_season_id_seasons_id_fk": { + "name": "teams_season_id_seasons_id_fk", + "tableFrom": "teams", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.season_status": { + "name": "season_status", + "schema": "public", + "values": [ + "pre_draft", + "draft", + "active", + "completed" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 3226a20..915f823 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1760165302772, "tag": "0001_serious_la_nuit", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1760166775427, + "tag": "0002_tired_iron_man", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 34f069a..37ad408 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "name": "brackt.com", "dependencies": { "@clerk/react-router": "^2.1.0", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-navigation-menu": "^1.2.14", @@ -22,10 +23,12 @@ "isbot": "^5.1.27", "lucide-react": "^0.545.0", "morgan": "^1.10.0", + "next-themes": "^0.4.6", "postgres": "^3.4.5", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router": "^7.7.1", + "sonner": "^2.0.7", "tailwind-merge": "^3.3.1" }, "devDependencies": { @@ -1714,6 +1717,34 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -5034,6 +5065,16 @@ "node": ">= 0.6" } }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/node-releases": { "version": "2.0.23", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", @@ -5900,6 +5941,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/package.json b/package.json index 1277cc9..dca557f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@clerk/react-router": "^2.1.0", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-navigation-menu": "^1.2.14", @@ -27,10 +28,12 @@ "isbot": "^5.1.27", "lucide-react": "^0.545.0", "morgan": "^1.10.0", + "next-themes": "^0.4.6", "postgres": "^3.4.5", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router": "^7.7.1", + "sonner": "^2.0.7", "tailwind-merge": "^3.3.1" }, "devDependencies": {