create-loadout 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -0
- package/dist/claude-md.d.ts +3 -0
- package/dist/claude-md.js +494 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +186 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +98 -0
- package/dist/create-next.d.ts +1 -0
- package/dist/create-next.js +17 -0
- package/dist/detect.d.ts +4 -0
- package/dist/detect.js +60 -0
- package/dist/env.d.ts +3 -0
- package/dist/env.js +183 -0
- package/dist/generate-readme.d.ts +3 -0
- package/dist/generate-readme.js +160 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/instrumentation.d.ts +3 -0
- package/dist/instrumentation.js +95 -0
- package/dist/integrations/ai-sdk.d.ts +3 -0
- package/dist/integrations/ai-sdk.js +20 -0
- package/dist/integrations/clerk.d.ts +2 -0
- package/dist/integrations/clerk.js +50 -0
- package/dist/integrations/firecrawl.d.ts +2 -0
- package/dist/integrations/firecrawl.js +26 -0
- package/dist/integrations/index.d.ts +4 -0
- package/dist/integrations/index.js +64 -0
- package/dist/integrations/inngest.d.ts +2 -0
- package/dist/integrations/inngest.js +45 -0
- package/dist/integrations/neon-drizzle.d.ts +2 -0
- package/dist/integrations/neon-drizzle.js +56 -0
- package/dist/integrations/posthog.d.ts +2 -0
- package/dist/integrations/posthog.js +25 -0
- package/dist/integrations/resend.d.ts +2 -0
- package/dist/integrations/resend.js +34 -0
- package/dist/integrations/sentry.d.ts +2 -0
- package/dist/integrations/sentry.js +47 -0
- package/dist/integrations/stripe.d.ts +2 -0
- package/dist/integrations/stripe.js +45 -0
- package/dist/integrations/uploadthing.d.ts +2 -0
- package/dist/integrations/uploadthing.js +34 -0
- package/dist/landing-page.d.ts +2 -0
- package/dist/landing-page.js +97 -0
- package/dist/prompts.d.ts +7 -0
- package/dist/prompts.js +99 -0
- package/dist/setup-shadcn.d.ts +1 -0
- package/dist/setup-shadcn.js +27 -0
- package/dist/templates/ai-sdk.d.ts +12 -0
- package/dist/templates/ai-sdk.js +96 -0
- package/dist/templates/clerk.d.ts +6 -0
- package/dist/templates/clerk.js +96 -0
- package/dist/templates/firecrawl.d.ts +4 -0
- package/dist/templates/firecrawl.js +106 -0
- package/dist/templates/inngest.d.ts +6 -0
- package/dist/templates/inngest.js +91 -0
- package/dist/templates/neon-drizzle.d.ts +16 -0
- package/dist/templates/neon-drizzle.js +343 -0
- package/dist/templates/posthog.d.ts +3 -0
- package/dist/templates/posthog.js +10 -0
- package/dist/templates/resend.d.ts +5 -0
- package/dist/templates/resend.js +102 -0
- package/dist/templates/sentry.d.ts +8 -0
- package/dist/templates/sentry.js +145 -0
- package/dist/templates/stripe.d.ts +6 -0
- package/dist/templates/stripe.js +215 -0
- package/dist/templates/uploadthing.d.ts +7 -0
- package/dist/templates/uploadthing.js +150 -0
- package/dist/templates/zustand.d.ts +3 -0
- package/dist/templates/zustand.js +26 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.js +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export const firecrawlTemplates = {
|
|
2
|
+
scrapeService: `import FirecrawlApp from '@mendable/firecrawl-js';
|
|
3
|
+
import { FIRECRAWL_API_KEY } from '@/lib/config';
|
|
4
|
+
|
|
5
|
+
export interface ScrapeResult {
|
|
6
|
+
markdown?: string;
|
|
7
|
+
html?: string;
|
|
8
|
+
metadata?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CrawlResult {
|
|
12
|
+
pages: ScrapeResult[];
|
|
13
|
+
total: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ScrapeService {
|
|
17
|
+
private client: FirecrawlApp;
|
|
18
|
+
|
|
19
|
+
constructor(apiKey: string) {
|
|
20
|
+
this.client = new FirecrawlApp({ apiKey });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async scrapeUrl(url: string): Promise<ScrapeResult> {
|
|
24
|
+
const result = await this.client.scrapeUrl(url, {
|
|
25
|
+
formats: ['markdown', 'html'],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!result.success) {
|
|
29
|
+
throw new Error(result.error || 'Failed to scrape URL');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
markdown: result.markdown,
|
|
34
|
+
html: result.html,
|
|
35
|
+
metadata: result.metadata,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async crawlSite(url: string, options?: { limit?: number }): Promise<CrawlResult> {
|
|
40
|
+
const result = await this.client.crawlUrl(url, {
|
|
41
|
+
limit: options?.limit ?? 10,
|
|
42
|
+
scrapeOptions: {
|
|
43
|
+
formats: ['markdown'],
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!result.success) {
|
|
48
|
+
throw new Error(result.error || 'Failed to crawl site');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
pages: result.data?.map((page) => ({
|
|
53
|
+
markdown: page.markdown,
|
|
54
|
+
metadata: page.metadata,
|
|
55
|
+
})) ?? [],
|
|
56
|
+
total: result.data?.length ?? 0,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async extractData<T>(url: string, schema: Record<string, unknown>): Promise<T> {
|
|
61
|
+
const result = await this.client.scrapeUrl(url, {
|
|
62
|
+
formats: ['extract'],
|
|
63
|
+
extract: { schema },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
throw new Error(result.error || 'Failed to extract data');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result.extract as T;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const scrapeService = new ScrapeService(FIRECRAWL_API_KEY);
|
|
75
|
+
`,
|
|
76
|
+
scrapeRoute: `import { NextResponse } from 'next/server';
|
|
77
|
+
import { scrapeService } from '@/services/scrape.service';
|
|
78
|
+
import { z } from 'zod';
|
|
79
|
+
|
|
80
|
+
const scrapeSchema = z.object({
|
|
81
|
+
url: z.url(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export async function POST(req: Request) {
|
|
85
|
+
try {
|
|
86
|
+
const body = await req.json();
|
|
87
|
+
const { url } = scrapeSchema.parse(body);
|
|
88
|
+
|
|
89
|
+
const result = await scrapeService.scrapeUrl(url);
|
|
90
|
+
|
|
91
|
+
return NextResponse.json(result);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error instanceof z.ZodError) {
|
|
94
|
+
return NextResponse.json(
|
|
95
|
+
{ error: 'Invalid request', details: error.errors },
|
|
96
|
+
{ status: 400 }
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return NextResponse.json(
|
|
100
|
+
{ error: error instanceof Error ? error.message : 'Failed to scrape' },
|
|
101
|
+
{ status: 500 }
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
`,
|
|
106
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const inngestTemplates = {
|
|
2
|
+
inngestClient: `import { Inngest } from 'inngest';
|
|
3
|
+
|
|
4
|
+
export const inngest = new Inngest({ id: 'my-app' });
|
|
5
|
+
`,
|
|
6
|
+
jobsService: `import { type Inngest } from 'inngest';
|
|
7
|
+
import { inngest } from '@/lib/inngest.client';
|
|
8
|
+
|
|
9
|
+
export class JobsService {
|
|
10
|
+
constructor(private client: Inngest) {}
|
|
11
|
+
|
|
12
|
+
async sendHelloWorld(name?: string): Promise<void> {
|
|
13
|
+
await this.client.send({
|
|
14
|
+
name: 'app/hello.world',
|
|
15
|
+
data: { name },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async syncUser(userId: string, email: string): Promise<void> {
|
|
20
|
+
await this.client.send({
|
|
21
|
+
name: 'app/user.sync',
|
|
22
|
+
data: { userId, email },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async sendEvent<T extends Record<string, unknown>>(
|
|
27
|
+
eventName: string,
|
|
28
|
+
data: T
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
await this.client.send({
|
|
31
|
+
name: eventName,
|
|
32
|
+
data,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const jobsService = new JobsService(inngest);
|
|
38
|
+
`,
|
|
39
|
+
inngestFunctions: `import { inngest } from './inngest.client';
|
|
40
|
+
|
|
41
|
+
export const helloWorld = inngest.createFunction(
|
|
42
|
+
{ id: 'hello-world' },
|
|
43
|
+
{ event: 'app/hello.world' },
|
|
44
|
+
async ({ event, step }) => {
|
|
45
|
+
const greeting = await step.run('create-greeting', async () => {
|
|
46
|
+
return \`Hello, \${event.data.name ?? 'World'}!\`;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await step.sleep('wait-a-moment', '1s');
|
|
50
|
+
|
|
51
|
+
return { message: greeting };
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
export const syncUser = inngest.createFunction(
|
|
56
|
+
{ id: 'sync-user' },
|
|
57
|
+
{ event: 'app/user.sync' },
|
|
58
|
+
async ({ event, step }) => {
|
|
59
|
+
const { userId, email } = event.data;
|
|
60
|
+
|
|
61
|
+
const userData = await step.run('fetch-user-data', async () => {
|
|
62
|
+
console.log('Syncing user:', userId, email);
|
|
63
|
+
return { synced: true };
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return userData;
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
export const dailyCleanup = inngest.createFunction(
|
|
71
|
+
{ id: 'daily-cleanup' },
|
|
72
|
+
{ cron: '0 0 * * *' },
|
|
73
|
+
async ({ step }) => {
|
|
74
|
+
await step.run('cleanup', async () => {
|
|
75
|
+
console.log('Running daily cleanup');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return { success: true };
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
`,
|
|
82
|
+
inngestRoute: `import { serve } from 'inngest/next';
|
|
83
|
+
import { inngest } from '@/lib/inngest.client';
|
|
84
|
+
import { helloWorld, syncUser, dailyCleanup } from '@/lib/inngest.functions';
|
|
85
|
+
|
|
86
|
+
export const { GET, POST, PUT } = serve({
|
|
87
|
+
client: inngest,
|
|
88
|
+
functions: [helloWorld, syncUser, dailyCleanup],
|
|
89
|
+
});
|
|
90
|
+
`,
|
|
91
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const neonDrizzleTemplates: {
|
|
2
|
+
drizzleConfig: string;
|
|
3
|
+
dbClient: string;
|
|
4
|
+
schema: string;
|
|
5
|
+
dbIndex: string;
|
|
6
|
+
todoDto: string;
|
|
7
|
+
todoView: string;
|
|
8
|
+
todoCreateSchema: string;
|
|
9
|
+
todoCreateState: string;
|
|
10
|
+
todoUpdateSchema: string;
|
|
11
|
+
todoUpdateState: string;
|
|
12
|
+
todoDao: string;
|
|
13
|
+
todoMapper: string;
|
|
14
|
+
todoService: string;
|
|
15
|
+
todoActions: string;
|
|
16
|
+
};
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
export const neonDrizzleTemplates = {
|
|
2
|
+
drizzleConfig: `import { defineConfig } from 'drizzle-kit';
|
|
3
|
+
import { DATABASE_URL } from './lib/config';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
schema: './lib/db/schema.ts',
|
|
7
|
+
out: './drizzle',
|
|
8
|
+
dialect: 'postgresql',
|
|
9
|
+
dbCredentials: {
|
|
10
|
+
url: DATABASE_URL,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
`,
|
|
14
|
+
dbClient: `import { neon } from '@neondatabase/serverless';
|
|
15
|
+
import { drizzle } from 'drizzle-orm/neon-http';
|
|
16
|
+
import * as schema from './schema';
|
|
17
|
+
import { DATABASE_URL } from '@/lib/config';
|
|
18
|
+
|
|
19
|
+
const sql = neon(DATABASE_URL);
|
|
20
|
+
|
|
21
|
+
export const db = drizzle({ client: sql, schema });
|
|
22
|
+
|
|
23
|
+
export { sql };
|
|
24
|
+
`,
|
|
25
|
+
schema: `import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
|
26
|
+
|
|
27
|
+
export const todos = pgTable('todos', {
|
|
28
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
29
|
+
title: text('title').notNull(),
|
|
30
|
+
description: text('description'),
|
|
31
|
+
completed: boolean('completed').notNull().default(false),
|
|
32
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
33
|
+
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
34
|
+
});
|
|
35
|
+
`,
|
|
36
|
+
dbIndex: `export { db, sql } from './client';
|
|
37
|
+
export * from './schema';
|
|
38
|
+
`,
|
|
39
|
+
todoDto: `import { todos } from '@/lib/db/schema';
|
|
40
|
+
import { InferInsertModel, InferSelectModel } from 'drizzle-orm';
|
|
41
|
+
|
|
42
|
+
export type TodoInsertDto = InferInsertModel<typeof todos>;
|
|
43
|
+
export type TodoDto = InferSelectModel<typeof todos>;
|
|
44
|
+
`,
|
|
45
|
+
todoView: `export type TodoView = {
|
|
46
|
+
id: string;
|
|
47
|
+
title: string;
|
|
48
|
+
description: string | null;
|
|
49
|
+
completed: boolean;
|
|
50
|
+
createdAt: Date;
|
|
51
|
+
};
|
|
52
|
+
`,
|
|
53
|
+
todoCreateSchema: `import { z } from 'zod';
|
|
54
|
+
|
|
55
|
+
export const TodoCreateFormSchema = z.object({
|
|
56
|
+
title: z.string().min(1, 'Title is required'),
|
|
57
|
+
description: z.string().optional(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export type TodoCreateFormPayload = z.infer<typeof TodoCreateFormSchema>;
|
|
61
|
+
|
|
62
|
+
export type TodoCreateServiceRequest = TodoCreateFormPayload;
|
|
63
|
+
|
|
64
|
+
export type TodoCreateServiceResult = {
|
|
65
|
+
todoId: string;
|
|
66
|
+
};
|
|
67
|
+
`,
|
|
68
|
+
todoCreateState: `export type TodoCreateState = {
|
|
69
|
+
success: boolean;
|
|
70
|
+
error: string | null;
|
|
71
|
+
data: { todoId: string } | null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const initialTodoCreateState: TodoCreateState = {
|
|
75
|
+
success: false,
|
|
76
|
+
error: null,
|
|
77
|
+
data: null,
|
|
78
|
+
};
|
|
79
|
+
`,
|
|
80
|
+
todoUpdateSchema: `import { z } from 'zod';
|
|
81
|
+
|
|
82
|
+
export const TodoUpdateFormSchema = z.object({
|
|
83
|
+
id: z.string().uuid(),
|
|
84
|
+
title: z.string().min(1, 'Title is required').optional(),
|
|
85
|
+
description: z.string().optional(),
|
|
86
|
+
completed: z
|
|
87
|
+
.string()
|
|
88
|
+
.transform((val) => val === 'true')
|
|
89
|
+
.optional(),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export type TodoUpdateFormPayload = z.infer<typeof TodoUpdateFormSchema>;
|
|
93
|
+
|
|
94
|
+
export type TodoUpdateServiceRequest = {
|
|
95
|
+
id: string;
|
|
96
|
+
title?: string;
|
|
97
|
+
description?: string;
|
|
98
|
+
completed?: boolean;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export type TodoUpdateServiceResult = {
|
|
102
|
+
todoId: string;
|
|
103
|
+
};
|
|
104
|
+
`,
|
|
105
|
+
todoUpdateState: `export type TodoUpdateState = {
|
|
106
|
+
success: boolean;
|
|
107
|
+
error: string | null;
|
|
108
|
+
data: { todoId: string } | null;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const initialTodoUpdateState: TodoUpdateState = {
|
|
112
|
+
success: false,
|
|
113
|
+
error: null,
|
|
114
|
+
data: null,
|
|
115
|
+
};
|
|
116
|
+
`,
|
|
117
|
+
todoDao: `import { eq } from 'drizzle-orm';
|
|
118
|
+
import { db } from '@/lib/db/client';
|
|
119
|
+
import { todos } from '@/lib/db/schema';
|
|
120
|
+
import type { TodoDto, TodoInsertDto } from '@/models/todo.dto';
|
|
121
|
+
|
|
122
|
+
export class TodoDAO {
|
|
123
|
+
async create(dto: TodoInsertDto): Promise<TodoDto | undefined> {
|
|
124
|
+
const [created] = await db.insert(todos).values(dto).returning();
|
|
125
|
+
return created;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async getById(id: string): Promise<TodoDto | undefined> {
|
|
129
|
+
return await db.query.todos.findFirst({
|
|
130
|
+
where: eq(todos.id, id),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async getAll(): Promise<TodoDto[]> {
|
|
135
|
+
return await db.query.todos.findMany({
|
|
136
|
+
orderBy: (todos, { desc }) => [desc(todos.createdAt)],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async update(id: string, dto: Partial<TodoInsertDto>): Promise<TodoDto | undefined> {
|
|
141
|
+
const [updated] = await db
|
|
142
|
+
.update(todos)
|
|
143
|
+
.set({ ...dto, updatedAt: new Date() })
|
|
144
|
+
.where(eq(todos.id, id))
|
|
145
|
+
.returning();
|
|
146
|
+
return updated;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async delete(id: string): Promise<void> {
|
|
150
|
+
await db.delete(todos).where(eq(todos.id, id));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const todoDAO = new TodoDAO();
|
|
155
|
+
`,
|
|
156
|
+
todoMapper: `import type { TodoDto, TodoInsertDto } from '@/models/todo.dto';
|
|
157
|
+
import type { TodoView } from '@/models/todo.view';
|
|
158
|
+
import type { TodoCreateServiceRequest } from '@/models/todoCreate.schema';
|
|
159
|
+
|
|
160
|
+
export class TodoMapper {
|
|
161
|
+
toInsertDto(request: TodoCreateServiceRequest): TodoInsertDto {
|
|
162
|
+
return {
|
|
163
|
+
title: request.title,
|
|
164
|
+
description: request.description ?? null,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
toView(dto: TodoDto): TodoView {
|
|
169
|
+
return {
|
|
170
|
+
id: dto.id,
|
|
171
|
+
title: dto.title,
|
|
172
|
+
description: dto.description,
|
|
173
|
+
completed: dto.completed,
|
|
174
|
+
createdAt: dto.createdAt,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
toViewList(dtos: TodoDto[]): TodoView[] {
|
|
179
|
+
return dtos.map((dto) => this.toView(dto));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const todoMapper = new TodoMapper();
|
|
184
|
+
`,
|
|
185
|
+
todoService: `import { todoDAO, type TodoDAO } from '@/dao/todo.dao';
|
|
186
|
+
import { todoMapper, type TodoMapper } from '@/mappers/todo.mapper';
|
|
187
|
+
import type { TodoView } from '@/models/todo.view';
|
|
188
|
+
import type { TodoCreateServiceRequest, TodoCreateServiceResult } from '@/models/todoCreate.schema';
|
|
189
|
+
import type { TodoUpdateServiceRequest, TodoUpdateServiceResult } from '@/models/todoUpdate.schema';
|
|
190
|
+
|
|
191
|
+
export class TodoService {
|
|
192
|
+
constructor(
|
|
193
|
+
private dao: TodoDAO,
|
|
194
|
+
private mapper: TodoMapper
|
|
195
|
+
) {}
|
|
196
|
+
|
|
197
|
+
async createTodo(request: TodoCreateServiceRequest): Promise<TodoCreateServiceResult> {
|
|
198
|
+
const insertDto = this.mapper.toInsertDto(request);
|
|
199
|
+
const created = await this.dao.create(insertDto);
|
|
200
|
+
|
|
201
|
+
if (!created) {
|
|
202
|
+
throw new Error('Failed to create todo');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { todoId: created.id };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async getTodoById(id: string): Promise<TodoView | null> {
|
|
209
|
+
const todo = await this.dao.getById(id);
|
|
210
|
+
return todo ? this.mapper.toView(todo) : null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async getAllTodos(): Promise<TodoView[]> {
|
|
214
|
+
const todos = await this.dao.getAll();
|
|
215
|
+
return this.mapper.toViewList(todos);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async updateTodo(request: TodoUpdateServiceRequest): Promise<TodoUpdateServiceResult> {
|
|
219
|
+
const updated = await this.dao.update(request.id, {
|
|
220
|
+
title: request.title,
|
|
221
|
+
description: request.description,
|
|
222
|
+
completed: request.completed,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (!updated) {
|
|
226
|
+
throw new Error('Todo not found');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { todoId: updated.id };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async toggleTodo(id: string): Promise<TodoView> {
|
|
233
|
+
const existing = await this.dao.getById(id);
|
|
234
|
+
|
|
235
|
+
if (!existing) {
|
|
236
|
+
throw new Error('Todo not found');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const updated = await this.dao.update(id, {
|
|
240
|
+
completed: !existing.completed,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return this.mapper.toView(updated!);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async deleteTodo(id: string): Promise<void> {
|
|
247
|
+
await this.dao.delete(id);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export const todoService = new TodoService(todoDAO, todoMapper);
|
|
252
|
+
`,
|
|
253
|
+
todoActions: `'use server';
|
|
254
|
+
|
|
255
|
+
import { revalidatePath } from 'next/cache';
|
|
256
|
+
import { z } from 'zod';
|
|
257
|
+
import { todoService } from '@/services/todo.service';
|
|
258
|
+
import { TodoCreateFormSchema } from '@/models/todoCreate.schema';
|
|
259
|
+
import { TodoUpdateFormSchema } from '@/models/todoUpdate.schema';
|
|
260
|
+
import type { TodoCreateState } from '@/models/todoCreate.state';
|
|
261
|
+
import type { TodoUpdateState } from '@/models/todoUpdate.state';
|
|
262
|
+
|
|
263
|
+
export async function createTodo(
|
|
264
|
+
state: TodoCreateState,
|
|
265
|
+
formData: FormData
|
|
266
|
+
): Promise<TodoCreateState> {
|
|
267
|
+
const rawData = Object.fromEntries(formData);
|
|
268
|
+
const validated = TodoCreateFormSchema.safeParse(rawData);
|
|
269
|
+
|
|
270
|
+
if (!validated.success) {
|
|
271
|
+
return {
|
|
272
|
+
success: false,
|
|
273
|
+
error: z.prettifyError(validated.error),
|
|
274
|
+
data: null,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const result = await todoService.createTodo(validated.data);
|
|
280
|
+
|
|
281
|
+
revalidatePath('/todos');
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
success: true,
|
|
285
|
+
error: null,
|
|
286
|
+
data: result,
|
|
287
|
+
};
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error('Error creating todo:', error);
|
|
290
|
+
return {
|
|
291
|
+
success: false,
|
|
292
|
+
error: error instanceof Error ? error.message : 'Failed to create todo',
|
|
293
|
+
data: null,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function updateTodo(
|
|
299
|
+
state: TodoUpdateState,
|
|
300
|
+
formData: FormData
|
|
301
|
+
): Promise<TodoUpdateState> {
|
|
302
|
+
const rawData = Object.fromEntries(formData);
|
|
303
|
+
const validated = TodoUpdateFormSchema.safeParse(rawData);
|
|
304
|
+
|
|
305
|
+
if (!validated.success) {
|
|
306
|
+
return {
|
|
307
|
+
success: false,
|
|
308
|
+
error: z.prettifyError(validated.error),
|
|
309
|
+
data: null,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const result = await todoService.updateTodo(validated.data);
|
|
315
|
+
|
|
316
|
+
revalidatePath('/todos');
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
success: true,
|
|
320
|
+
error: null,
|
|
321
|
+
data: result,
|
|
322
|
+
};
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error('Error updating todo:', error);
|
|
325
|
+
return {
|
|
326
|
+
success: false,
|
|
327
|
+
error: error instanceof Error ? error.message : 'Failed to update todo',
|
|
328
|
+
data: null,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export async function toggleTodo(id: string): Promise<void> {
|
|
334
|
+
await todoService.toggleTodo(id);
|
|
335
|
+
revalidatePath('/todos');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export async function deleteTodo(id: string): Promise<void> {
|
|
339
|
+
await todoService.deleteTodo(id);
|
|
340
|
+
revalidatePath('/todos');
|
|
341
|
+
}
|
|
342
|
+
`,
|
|
343
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export const resendTemplates = {
|
|
2
|
+
emailService: `import { Resend } from 'resend';
|
|
3
|
+
import type { ReactElement } from 'react';
|
|
4
|
+
import { RESEND_API_KEY, RESEND_FROM_EMAIL } from '@/lib/config';
|
|
5
|
+
|
|
6
|
+
export interface SendEmailOptions {
|
|
7
|
+
to: string | string[];
|
|
8
|
+
subject: string;
|
|
9
|
+
react: ReactElement;
|
|
10
|
+
from?: string;
|
|
11
|
+
replyTo?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class EmailService {
|
|
15
|
+
private resend: Resend;
|
|
16
|
+
private defaultFrom: string;
|
|
17
|
+
|
|
18
|
+
constructor(apiKey: string, defaultFrom: string) {
|
|
19
|
+
this.resend = new Resend(apiKey);
|
|
20
|
+
this.defaultFrom = defaultFrom;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async send(options: SendEmailOptions) {
|
|
24
|
+
const { data, error } = await this.resend.emails.send({
|
|
25
|
+
from: options.from || this.defaultFrom,
|
|
26
|
+
to: options.to,
|
|
27
|
+
subject: options.subject,
|
|
28
|
+
react: options.react,
|
|
29
|
+
replyTo: options.replyTo,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (error) {
|
|
33
|
+
throw new Error(error.message);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async sendWelcome(to: string, name: string) {
|
|
40
|
+
const { WelcomeEmail } = await import('@/components/emails/welcome');
|
|
41
|
+
return this.send({
|
|
42
|
+
to,
|
|
43
|
+
subject: 'Welcome!',
|
|
44
|
+
react: WelcomeEmail({ name }),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const emailService = new EmailService(RESEND_API_KEY, RESEND_FROM_EMAIL);
|
|
50
|
+
`,
|
|
51
|
+
welcomeEmail: `import * as React from 'react';
|
|
52
|
+
|
|
53
|
+
interface WelcomeEmailProps {
|
|
54
|
+
name: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function WelcomeEmail({ name }: WelcomeEmailProps) {
|
|
58
|
+
return (
|
|
59
|
+
<div style={{ fontFamily: 'sans-serif', padding: '20px' }}>
|
|
60
|
+
<h1 style={{ color: '#333' }}>Welcome, {name}!</h1>
|
|
61
|
+
<p style={{ color: '#666', lineHeight: 1.6 }}>
|
|
62
|
+
Thanks for signing up. We're excited to have you on board.
|
|
63
|
+
</p>
|
|
64
|
+
<p style={{ color: '#666', lineHeight: 1.6 }}>
|
|
65
|
+
If you have any questions, feel free to reply to this email.
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
`,
|
|
71
|
+
sendRoute: `import { NextResponse } from 'next/server';
|
|
72
|
+
import { emailService } from '@/services/email.service';
|
|
73
|
+
import { z } from 'zod';
|
|
74
|
+
|
|
75
|
+
const sendEmailSchema = z.object({
|
|
76
|
+
to: z.email(),
|
|
77
|
+
name: z.string().min(1),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export async function POST(req: Request) {
|
|
81
|
+
try {
|
|
82
|
+
const body = await req.json();
|
|
83
|
+
const { to, name } = sendEmailSchema.parse(body);
|
|
84
|
+
|
|
85
|
+
const data = await emailService.sendWelcome(to, name);
|
|
86
|
+
|
|
87
|
+
return NextResponse.json(data);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error instanceof z.ZodError) {
|
|
90
|
+
return NextResponse.json(
|
|
91
|
+
{ error: 'Invalid request', details: error.errors },
|
|
92
|
+
{ status: 400 }
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return NextResponse.json(
|
|
96
|
+
{ error: error instanceof Error ? error.message : 'Failed to send email' },
|
|
97
|
+
{ status: 500 }
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
`,
|
|
102
|
+
};
|