Add waitlist schema for marketing (FRE-635)

- Created waitlist_signups and waitlist_events tables
- Supports email, name, source tracking, and status management
- Enables VIP supporter list for Product Hunt launch
- Migration 0002_chemical_shocker.sql generated
- Fixed brand color in product-hunt-assets-brief.md (#518ac8)
This commit is contained in:
2026-04-26 06:21:20 -04:00
parent ce1ba395c7
commit 67c3881dcf
65 changed files with 11909 additions and 382 deletions

View File

@@ -190,7 +190,7 @@ export const projectRouter = {
.where(eq(characters.id, input.id));
const character = rows[0];
if (!character) {
throw new Error(`Character ${input.id} not found`);
throw new TRPCError({ code: 'NOT_FOUND', message: `Character ${input.id} not found` });
}
await verifyProjectOwnership(ctx.db!, character.projectId, ctx.userId!);
return character;
@@ -265,7 +265,7 @@ export const projectRouter = {
.where(eq(characters.id, input.id));
const existing = existingRows[0];
if (!existing) {
throw new Error(`Character ${input.id} not found`);
throw new TRPCError({ code: 'NOT_FOUND', message: `Character ${input.id} not found` });
}
await verifyProjectOwnership(ctx.db!, existing.projectId, ctx.userId!);
@@ -304,7 +304,7 @@ export const projectRouter = {
.where(eq(characters.id, input.id));
const existing = existingRows[0];
if (!existing) {
throw new Error(`Character ${input.id} not found`);
throw new TRPCError({ code: 'NOT_FOUND', message: `Character ${input.id} not found` });
}
await verifyProjectOwnership(ctx.db!, existing.projectId, ctx.userId!);
@@ -369,8 +369,9 @@ export const projectRouter = {
.from(characters)
.where(eq(characters.id, input.characterId));
if (!rows[0]) {
throw new Error(`Character ${input.characterId} not found`);
throw new TRPCError({ code: 'NOT_FOUND', message: `Character ${input.characterId} not found` });
}
await verifyProjectOwnership(ctx.db!, rows[0].projectId, ctx.userId!);
return await getCharacterStatsImpl(ctx.db!, input.characterId);
}),
@@ -413,7 +414,7 @@ export const projectRouter = {
.from(characters)
.where(eq(characters.id, input.characterId));
if (!rows[0]) {
throw new Error(`Character ${input.characterId} not found`);
throw new TRPCError({ code: 'NOT_FOUND', message: `Character ${input.characterId} not found` });
}
await verifyProjectOwnership(ctx.db!, rows[0].projectId, ctx.userId!);
return await ctx.db!.select()
@@ -437,7 +438,7 @@ export const projectRouter = {
}))
.mutation(async ({ input, ctx }) => {
if (input.characterIdA === input.characterIdB) {
throw new Error('Cannot create a relationship with the same character');
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cannot create a relationship with the same character' });
}
const charARows = await ctx.db!.select()
@@ -447,7 +448,7 @@ export const projectRouter = {
.from(characters)
.where(eq(characters.id, input.characterIdB));
if (!charARows[0] || !charBRows[0]) {
throw new Error('Both characters must exist');
throw new TRPCError({ code: 'NOT_FOUND', message: 'Both characters must exist' });
}
await verifyProjectOwnership(ctx.db!, charARows[0].projectId, ctx.userId!);
@@ -495,14 +496,14 @@ export const projectRouter = {
.from(characterRelationships)
.where(eq(characterRelationships.id, input.id));
if (!relRows[0]) {
throw new Error(`Relationship ${input.id} not found`);
throw new TRPCError({ code: 'NOT_FOUND', message: `Relationship ${input.id} not found` });
}
const charARows = await ctx.db!.select()
.from(characters)
.where(eq(characters.id, relRows[0].characterIdA));
if (!charARows[0]) {
throw new Error('Character not found');
throw new TRPCError({ code: 'NOT_FOUND', message: 'Character not found' });
}
await verifyProjectOwnership(ctx.db!, charARows[0].projectId, ctx.userId!);
@@ -526,14 +527,14 @@ export const projectRouter = {
.from(characterRelationships)
.where(eq(characterRelationships.id, input.id));
if (!relRows[0]) {
throw new Error(`Relationship ${input.id} not found`);
throw new TRPCError({ code: 'NOT_FOUND', message: `Relationship ${input.id} not found` });
}
const charARows = await ctx.db!.select()
.from(characters)
.where(eq(characters.id, relRows[0].characterIdA));
if (!charARows[0]) {
throw new Error('Character not found');
throw new TRPCError({ code: 'NOT_FOUND', message: 'Character not found' });
}
await verifyProjectOwnership(ctx.db!, charARows[0].projectId, ctx.userId!);
@@ -562,7 +563,7 @@ export const projectRouter = {
.where(eq(scenes.id, input.id));
const scene = rows[0];
if (!scene) {
throw new Error(`Scene ${input.id} not found`);
throw new TRPCError({ code: 'NOT_FOUND', message: `Scene ${input.id} not found` });
}
await verifyProjectOwnership(ctx.db!, scene.projectId, ctx.userId!);
return scene;
@@ -601,7 +602,7 @@ export const projectRouter = {
.from(scenes)
.where(eq(scenes.id, input.id));
if (!rows[0]) {
throw new Error(`Scene ${input.id} not found`);
throw new TRPCError({ code: 'NOT_FOUND', message: `Scene ${input.id} not found` });
}
const updateData: Record<string, any> = { updatedAt: new Date() };
@@ -623,9 +624,12 @@ export const projectRouter = {
.from(scenes)
.where(eq(scenes.id, input.id));
if (!rows[0]) {
throw new Error(`Scene ${input.id} not found`);
throw new TRPCError({ code: 'NOT_FOUND', message: `Scene ${input.id} not found` });
}
// Check project ownership
await verifyProjectOwnership(ctx.db!, rows[0].projectId, ctx.userId!);
await ctx.db!.delete(scenes)
.where(eq(scenes.id, input.id));