feat: add user model, routes, and Clerk webhook integration
This commit is contained in:
parent
9dc033b2a0
commit
9749fc8b77
13 changed files with 1069 additions and 0 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
// Re-export all model functions and types
|
// Re-export all model functions and types
|
||||||
|
export * from "./user";
|
||||||
export * from "./league";
|
export * from "./league";
|
||||||
export * from "./season";
|
export * from "./season";
|
||||||
export * from "./team";
|
export * from "./team";
|
||||||
|
|
|
||||||
108
app/models/user.ts
Normal file
108
app/models/user.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { database } from "~/database/context";
|
||||||
|
import * as schema from "~/database/schema";
|
||||||
|
|
||||||
|
export type User = typeof schema.users.$inferSelect;
|
||||||
|
export type NewUser = typeof schema.users.$inferInsert;
|
||||||
|
|
||||||
|
export async function createUser(data: NewUser): Promise<User> {
|
||||||
|
const db = database();
|
||||||
|
const [user] = await db.insert(schema.users).values(data).returning();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findUserById(id: string): Promise<User | undefined> {
|
||||||
|
const db = database();
|
||||||
|
return await db.query.users.findFirst({
|
||||||
|
where: eq(schema.users.id, id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findUserByClerkId(
|
||||||
|
clerkId: string
|
||||||
|
): Promise<User | undefined> {
|
||||||
|
const db = database();
|
||||||
|
return await db.query.users.findFirst({
|
||||||
|
where: eq(schema.users.clerkId, clerkId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUser(
|
||||||
|
id: string,
|
||||||
|
data: Partial<NewUser>
|
||||||
|
): Promise<User> {
|
||||||
|
const db = database();
|
||||||
|
const [user] = await db
|
||||||
|
.update(schema.users)
|
||||||
|
.set({ ...data, updatedAt: new Date() })
|
||||||
|
.where(eq(schema.users.id, id))
|
||||||
|
.returning();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUserByClerkId(
|
||||||
|
clerkId: string,
|
||||||
|
data: Partial<NewUser>
|
||||||
|
): Promise<User> {
|
||||||
|
const db = database();
|
||||||
|
const [user] = await db
|
||||||
|
.update(schema.users)
|
||||||
|
.set({ ...data, updatedAt: new Date() })
|
||||||
|
.where(eq(schema.users.clerkId, clerkId))
|
||||||
|
.returning();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findOrCreateUser(clerkUser: {
|
||||||
|
id: string;
|
||||||
|
emailAddresses?: Array<{ emailAddress: string }>;
|
||||||
|
firstName?: string | null;
|
||||||
|
lastName?: string | null;
|
||||||
|
imageUrl?: string;
|
||||||
|
}): Promise<User> {
|
||||||
|
// Try to find existing user
|
||||||
|
const existingUser = await findUserByClerkId(clerkUser.id);
|
||||||
|
|
||||||
|
const email = clerkUser.emailAddresses?.[0]?.emailAddress;
|
||||||
|
if (!email) {
|
||||||
|
throw new Error("User must have an email address");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate display name: firstName lastName, or email username
|
||||||
|
let displayName = "";
|
||||||
|
if (clerkUser.firstName || clerkUser.lastName) {
|
||||||
|
displayName = [clerkUser.firstName, clerkUser.lastName]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")
|
||||||
|
.trim();
|
||||||
|
} else {
|
||||||
|
// Use email username as fallback
|
||||||
|
displayName = email.split("@")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
// Update user info in case it changed in Clerk
|
||||||
|
return await updateUserByClerkId(clerkUser.id, {
|
||||||
|
email,
|
||||||
|
displayName,
|
||||||
|
firstName: clerkUser.firstName || undefined,
|
||||||
|
lastName: clerkUser.lastName || undefined,
|
||||||
|
imageUrl: clerkUser.imageUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new user
|
||||||
|
return await createUser({
|
||||||
|
clerkId: clerkUser.id,
|
||||||
|
email,
|
||||||
|
displayName,
|
||||||
|
firstName: clerkUser.firstName || undefined,
|
||||||
|
lastName: clerkUser.lastName || undefined,
|
||||||
|
imageUrl: clerkUser.imageUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteUser(id: string): Promise<void> {
|
||||||
|
const db = database();
|
||||||
|
await db.delete(schema.users).where(eq(schema.users.id, id));
|
||||||
|
}
|
||||||
|
|
@ -5,4 +5,6 @@ export default [
|
||||||
route("leagues/new", "routes/leagues/new.tsx"),
|
route("leagues/new", "routes/leagues/new.tsx"),
|
||||||
route("leagues/:leagueId", "routes/leagues/$leagueId.tsx"),
|
route("leagues/:leagueId", "routes/leagues/$leagueId.tsx"),
|
||||||
route("leagues/:leagueId/settings", "routes/leagues/$leagueId.settings.tsx"),
|
route("leagues/:leagueId/settings", "routes/leagues/$leagueId.settings.tsx"),
|
||||||
|
route("api/webhooks/clerk", "routes/api/webhooks/clerk.ts"),
|
||||||
|
route("user-profile", "routes/user-profile.tsx"),
|
||||||
] satisfies RouteConfig;
|
] satisfies RouteConfig;
|
||||||
|
|
|
||||||
94
app/routes/api/webhooks/clerk.ts
Normal file
94
app/routes/api/webhooks/clerk.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { Webhook } from "svix";
|
||||||
|
import type { Route } from "./+types/clerk";
|
||||||
|
import { findOrCreateUser, updateUserByClerkId } from "~/models/user";
|
||||||
|
|
||||||
|
export async function action({ request }: Route.ActionArgs) {
|
||||||
|
// Get the webhook secret from environment
|
||||||
|
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;
|
||||||
|
|
||||||
|
if (!WEBHOOK_SECRET) {
|
||||||
|
throw new Error("Please add CLERK_WEBHOOK_SECRET to your .env file");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the headers
|
||||||
|
const svix_id = request.headers.get("svix-id");
|
||||||
|
const svix_timestamp = request.headers.get("svix-timestamp");
|
||||||
|
const svix_signature = request.headers.get("svix-signature");
|
||||||
|
|
||||||
|
// If there are no headers, error out
|
||||||
|
if (!svix_id || !svix_timestamp || !svix_signature) {
|
||||||
|
return new Response("Error: Missing svix headers", { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the body
|
||||||
|
const payload = await request.text();
|
||||||
|
|
||||||
|
// Create a new Svix instance with your webhook secret
|
||||||
|
const wh = new Webhook(WEBHOOK_SECRET);
|
||||||
|
|
||||||
|
let evt: any;
|
||||||
|
|
||||||
|
// Verify the webhook
|
||||||
|
try {
|
||||||
|
evt = wh.verify(payload, {
|
||||||
|
"svix-id": svix_id,
|
||||||
|
"svix-timestamp": svix_timestamp,
|
||||||
|
"svix-signature": svix_signature,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error verifying webhook:", err);
|
||||||
|
return new Response("Error: Verification failed", { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the webhook
|
||||||
|
const eventType = evt.type;
|
||||||
|
console.log(`Webhook received: ${eventType}`);
|
||||||
|
|
||||||
|
if (eventType === "user.created" || eventType === "user.updated") {
|
||||||
|
const { id, email_addresses, first_name, last_name, image_url } = evt.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (eventType === "user.created") {
|
||||||
|
// Create new user
|
||||||
|
const user = await findOrCreateUser({
|
||||||
|
id,
|
||||||
|
emailAddresses: email_addresses,
|
||||||
|
firstName: first_name,
|
||||||
|
lastName: last_name,
|
||||||
|
imageUrl: image_url,
|
||||||
|
});
|
||||||
|
console.log(`User created in database: ${user.id} (${user.displayName})`);
|
||||||
|
} else {
|
||||||
|
// Update existing user
|
||||||
|
const email = email_addresses?.[0]?.email_address;
|
||||||
|
if (!email) {
|
||||||
|
throw new Error("User must have an email address");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate display name
|
||||||
|
let displayName = "";
|
||||||
|
if (first_name || last_name) {
|
||||||
|
displayName = [first_name, last_name].filter(Boolean).join(" ").trim();
|
||||||
|
} else {
|
||||||
|
displayName = email.split("@")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await updateUserByClerkId(id, {
|
||||||
|
email,
|
||||||
|
displayName,
|
||||||
|
firstName: first_name || undefined,
|
||||||
|
lastName: last_name || undefined,
|
||||||
|
imageUrl: image_url,
|
||||||
|
});
|
||||||
|
console.log(`User updated in database: ${user.id} (${user.displayName})`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error handling ${eventType}:`, error);
|
||||||
|
return new Response(`Error: Failed to ${eventType === "user.created" ? "create" : "update"} user`, {
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response("Webhook processed successfully", { status: 200 });
|
||||||
|
}
|
||||||
22
app/routes/user-profile.tsx
Normal file
22
app/routes/user-profile.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { UserProfile } from "@clerk/react-router";
|
||||||
|
import { getAuth } from "@clerk/react-router/server";
|
||||||
|
import { redirect } from "react-router";
|
||||||
|
import type { Route } from "./+types/user-profile";
|
||||||
|
|
||||||
|
export async function loader(args: Route.LoaderArgs) {
|
||||||
|
const { userId } = await getAuth(args);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return redirect("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserProfilePage() {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-8 px-4 flex justify-center">
|
||||||
|
<UserProfile />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,19 @@ export const guestBook = pgTable("guestBook", {
|
||||||
email: varchar({ length: 255 }).notNull().unique(),
|
email: varchar({ length: 255 }).notNull().unique(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Users table - synced from Clerk
|
||||||
|
export const users = pgTable("users", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
clerkId: varchar("clerk_id", { length: 255 }).notNull().unique(),
|
||||||
|
email: varchar("email", { length: 255 }).notNull(),
|
||||||
|
displayName: varchar("display_name", { length: 255 }),
|
||||||
|
firstName: varchar("first_name", { length: 255 }),
|
||||||
|
lastName: varchar("last_name", { length: 255 }),
|
||||||
|
imageUrl: varchar("image_url", { length: 512 }),
|
||||||
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
// Fantasy League Tables
|
// Fantasy League Tables
|
||||||
export const seasonStatusEnum = pgEnum("season_status", [
|
export const seasonStatusEnum = pgEnum("season_status", [
|
||||||
"pre_draft",
|
"pre_draft",
|
||||||
|
|
|
||||||
11
drizzle/0003_public_shadowcat.sql
Normal file
11
drizzle/0003_public_shadowcat.sql
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS "users" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"clerk_id" varchar(255) NOT NULL,
|
||||||
|
"email" varchar(255),
|
||||||
|
"first_name" varchar(255),
|
||||||
|
"last_name" varchar(255),
|
||||||
|
"image_url" varchar(512),
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "users_clerk_id_unique" UNIQUE("clerk_id")
|
||||||
|
);
|
||||||
2
drizzle/0004_spicy_zarda.sql
Normal file
2
drizzle/0004_spicy_zarda.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE "users" ALTER COLUMN "email" SET NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" ADD COLUMN "display_name" varchar(255);
|
||||||
385
drizzle/meta/0003_snapshot.json
Normal file
385
drizzle/meta/0003_snapshot.json
Normal file
|
|
@ -0,0 +1,385 @@
|
||||||
|
{
|
||||||
|
"id": "db985c81-11e6-4c1b-9cf0-0fffec7bea16",
|
||||||
|
"prevId": "34297d0b-9216-46c7-9d1d-5b81f1402786",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"public.users": {
|
||||||
|
"name": "users",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"clerk_id": {
|
||||||
|
"name": "clerk_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"first_name": {
|
||||||
|
"name": "first_name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"last_name": {
|
||||||
|
"name": "last_name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"image_url": {
|
||||||
|
"name": "image_url",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"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": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_clerk_id_unique": {
|
||||||
|
"name": "users_clerk_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"clerk_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
391
drizzle/meta/0004_snapshot.json
Normal file
391
drizzle/meta/0004_snapshot.json
Normal file
|
|
@ -0,0 +1,391 @@
|
||||||
|
{
|
||||||
|
"id": "5e858241-1709-4099-90d2-58c7e3bf0d23",
|
||||||
|
"prevId": "db985c81-11e6-4c1b-9cf0-0fffec7bea16",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"public.users": {
|
||||||
|
"name": "users",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"clerk_id": {
|
||||||
|
"name": "clerk_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"display_name": {
|
||||||
|
"name": "display_name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"first_name": {
|
||||||
|
"name": "first_name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"last_name": {
|
||||||
|
"name": "last_name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"image_url": {
|
||||||
|
"name": "image_url",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"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": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_clerk_id_unique": {
|
||||||
|
"name": "users_clerk_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"clerk_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,20 @@
|
||||||
"when": 1760166775427,
|
"when": 1760166775427,
|
||||||
"tag": "0002_tired_iron_man",
|
"tag": "0002_tired_iron_man",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1760167848365,
|
||||||
|
"tag": "0003_public_shadowcat",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1760168383812,
|
||||||
|
"tag": "0004_spicy_zarda",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
25
package-lock.json
generated
25
package-lock.json
generated
|
|
@ -29,6 +29,7 @@
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router": "^7.7.1",
|
"react-router": "^7.7.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
|
"svix": "^1.77.0",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -6147,6 +6148,17 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svix": {
|
||||||
|
"version": "1.77.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/svix/-/svix-1.77.0.tgz",
|
||||||
|
"integrity": "sha512-rqyvcFHMq1eGIjYwZEEsW5MkeLH4FRr23TuSsLLhH+/wilK4sjdJSYmALTke3kyMqab7lqWTc9jyKFw6o0/oKg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@stablelib/base64": "^1.0.0",
|
||||||
|
"fast-sha256": "^1.3.0",
|
||||||
|
"uuid": "^10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/swr": {
|
"node_modules/swr": {
|
||||||
"version": "2.3.4",
|
"version": "2.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.4.tgz",
|
||||||
|
|
@ -6861,6 +6873,19 @@
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/valibot": {
|
"node_modules/valibot": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router": "^7.7.1",
|
"react-router": "^7.7.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
|
"svix": "^1.77.0",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue