create-m5kdev 0.4.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/dist/src/__tests__/create.smoke.test.d.ts +1 -0
- package/dist/src/__tests__/create.smoke.test.js +56 -0
- package/dist/src/__tests__/create.test.d.ts +1 -0
- package/dist/src/__tests__/create.test.js +55 -0
- package/dist/src/__tests__/runCli.test.d.ts +1 -0
- package/dist/src/__tests__/runCli.test.js +44 -0
- package/dist/src/__tests__/strings.test.d.ts +1 -0
- package/dist/src/__tests__/strings.test.js +24 -0
- package/dist/src/constants.d.ts +3 -0
- package/dist/src/constants.js +9 -0
- package/dist/src/create.d.ts +7 -0
- package/dist/src/create.js +36 -0
- package/dist/src/fs.d.ts +5 -0
- package/dist/src/fs.js +60 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +9 -0
- package/dist/src/paths.d.ts +1 -0
- package/dist/src/paths.js +14 -0
- package/dist/src/prompts.d.ts +2 -0
- package/dist/src/prompts.js +55 -0
- package/dist/src/runCli.d.ts +9 -0
- package/dist/src/runCli.js +107 -0
- package/dist/src/strings.d.ts +6 -0
- package/dist/src/strings.js +47 -0
- package/dist/src/types.d.ts +18 -0
- package/dist/src/types.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +38 -0
- package/templates/minimal-app/.gitignore.tpl +9 -0
- package/templates/minimal-app/AGENTS.md.tpl +29 -0
- package/templates/minimal-app/README.md.tpl +35 -0
- package/templates/minimal-app/apps/email/package.json.tpl +27 -0
- package/templates/minimal-app/apps/email/src/components/BaseEmail.tsx.tpl +117 -0
- package/templates/minimal-app/apps/email/src/emails/accountDeletionEmail.tsx.tpl +26 -0
- package/templates/minimal-app/apps/email/src/emails/organizationInviteEmail.tsx.tpl +31 -0
- package/templates/minimal-app/apps/email/src/emails/passwordResetEmail.tsx.tpl +25 -0
- package/templates/minimal-app/apps/email/src/emails/verificationEmail.tsx.tpl +26 -0
- package/templates/minimal-app/apps/email/src/index.ts.tpl +32 -0
- package/templates/minimal-app/apps/email/tsconfig.json.tpl +11 -0
- package/templates/minimal-app/apps/server/AGENTS.md.tpl +30 -0
- package/templates/minimal-app/apps/server/drizzle/seed.ts.tpl +111 -0
- package/templates/minimal-app/apps/server/drizzle/sync.ts.tpl +18 -0
- package/templates/minimal-app/apps/server/drizzle.config.ts.tpl +38 -0
- package/templates/minimal-app/apps/server/package.json.tpl +50 -0
- package/templates/minimal-app/apps/server/src/db.ts.tpl +33 -0
- package/templates/minimal-app/apps/server/src/index.ts.tpl +34 -0
- package/templates/minimal-app/apps/server/src/lib/auth.ts.tpl +172 -0
- package/templates/minimal-app/apps/server/src/lib/localEmailService.ts.tpl +58 -0
- package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +23 -0
- package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +106 -0
- package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +150 -0
- package/templates/minimal-app/apps/server/src/modules/posts/posts.trpc.ts.tpl +44 -0
- package/templates/minimal-app/apps/server/src/repository.ts.tpl +6 -0
- package/templates/minimal-app/apps/server/src/service.ts.tpl +12 -0
- package/templates/minimal-app/apps/server/src/trpc.ts.tpl +11 -0
- package/templates/minimal-app/apps/server/src/types.ts.tpl +3 -0
- package/templates/minimal-app/apps/server/src/utils/trpc.ts.tpl +27 -0
- package/templates/minimal-app/apps/server/tsconfig.json.tpl +13 -0
- package/templates/minimal-app/apps/server/tsup.config.ts.tpl +9 -0
- package/templates/minimal-app/apps/shared/.env.example.tpl +17 -0
- package/templates/minimal-app/apps/shared/.env.tpl +19 -0
- package/templates/minimal-app/apps/shared/package.json.tpl +24 -0
- package/templates/minimal-app/apps/shared/src/modules/posts/posts.constants.ts.tpl +5 -0
- package/templates/minimal-app/apps/shared/src/modules/posts/posts.schema.ts.tpl +76 -0
- package/templates/minimal-app/apps/shared/tsconfig.json.tpl +12 -0
- package/templates/minimal-app/apps/webapp/AGENTS.md.tpl +18 -0
- package/templates/minimal-app/apps/webapp/index.html.tpl +22 -0
- package/templates/minimal-app/apps/webapp/package.json.tpl +52 -0
- package/templates/minimal-app/apps/webapp/src/App.tsx.tpl +13 -0
- package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +139 -0
- package/templates/minimal-app/apps/webapp/src/Providers.tsx.tpl +28 -0
- package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +53 -0
- package/templates/minimal-app/apps/webapp/src/components/TrpcQueryProvider.tsx.tpl +61 -0
- package/templates/minimal-app/apps/webapp/src/hero.ts.tpl +99 -0
- package/templates/minimal-app/apps/webapp/src/index.css.tpl +75 -0
- package/templates/minimal-app/apps/webapp/src/main.tsx.tpl +26 -0
- package/templates/minimal-app/apps/webapp/src/modules/posts/PostsRoute.tsx.tpl +650 -0
- package/templates/minimal-app/apps/webapp/src/utils/i18n.ts.tpl +13 -0
- package/templates/minimal-app/apps/webapp/src/utils/trpc.ts.tpl +4 -0
- package/templates/minimal-app/apps/webapp/src/vite-env.d.ts.tpl +1 -0
- package/templates/minimal-app/apps/webapp/translations/en/blog-app.json.tpl +107 -0
- package/templates/minimal-app/apps/webapp/tsconfig.json.tpl +16 -0
- package/templates/minimal-app/apps/webapp/vite.config.ts.tpl +31 -0
- package/templates/minimal-app/biome.json.tpl +76 -0
- package/templates/minimal-app/package.json.tpl +21 -0
- package/templates/minimal-app/pnpm-workspace.yaml.tpl +58 -0
- package/templates/minimal-app/turbo.json.tpl +26 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PostSchema,
|
|
3
|
+
PostsListInputSchema,
|
|
4
|
+
PostsListOutputSchema,
|
|
5
|
+
} from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.schema";
|
|
6
|
+
import { BaseTableRepository } from "@m5kdev/backend/modules/base/base.repository";
|
|
7
|
+
import type { ServerResultAsync } from "@m5kdev/backend/utils/types";
|
|
8
|
+
import { and, asc, count, desc, eq, isNull, like, ne, or } from "drizzle-orm";
|
|
9
|
+
import { ok } from "neverthrow";
|
|
10
|
+
import type { Orm, Schema } from "../../db";
|
|
11
|
+
|
|
12
|
+
export class PostsRepository extends BaseTableRepository<
|
|
13
|
+
Orm,
|
|
14
|
+
Schema,
|
|
15
|
+
Record<string, never>,
|
|
16
|
+
Schema["posts"]
|
|
17
|
+
> {
|
|
18
|
+
async list(input: PostsListInputSchema = {}): ServerResultAsync<PostsListOutputSchema> {
|
|
19
|
+
return this.throwableAsync(async () => {
|
|
20
|
+
const page = input.page ?? 1;
|
|
21
|
+
const limit = input.limit ?? 6;
|
|
22
|
+
const search = input.search?.trim();
|
|
23
|
+
const conditions = [isNull(this.table.deletedAt)];
|
|
24
|
+
|
|
25
|
+
if (input.status) {
|
|
26
|
+
conditions.push(eq(this.table.status, input.status));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (search) {
|
|
30
|
+
const pattern = `%${search}%`;
|
|
31
|
+
const searchCondition = or(
|
|
32
|
+
like(this.table.title, pattern),
|
|
33
|
+
like(this.table.slug, pattern),
|
|
34
|
+
like(this.table.excerpt, pattern),
|
|
35
|
+
like(this.table.content, pattern)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (searchCondition) {
|
|
39
|
+
conditions.push(searchCondition);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const whereClause = and(...conditions);
|
|
44
|
+
const ordering =
|
|
45
|
+
input.sort === "title"
|
|
46
|
+
? input.order === "asc"
|
|
47
|
+
? asc(this.table.title)
|
|
48
|
+
: desc(this.table.title)
|
|
49
|
+
: input.sort === "publishedAt"
|
|
50
|
+
? input.order === "asc"
|
|
51
|
+
? asc(this.table.publishedAt)
|
|
52
|
+
: desc(this.table.publishedAt)
|
|
53
|
+
: input.order === "asc"
|
|
54
|
+
? asc(this.table.updatedAt)
|
|
55
|
+
: desc(this.table.updatedAt);
|
|
56
|
+
|
|
57
|
+
const rows = await this.orm
|
|
58
|
+
.select()
|
|
59
|
+
.from(this.table)
|
|
60
|
+
.where(whereClause)
|
|
61
|
+
.orderBy(ordering, desc(this.table.createdAt))
|
|
62
|
+
.limit(limit)
|
|
63
|
+
.offset((page - 1) * limit);
|
|
64
|
+
|
|
65
|
+
const [totalRow] = await this.orm
|
|
66
|
+
.select({ count: count() })
|
|
67
|
+
.from(this.table)
|
|
68
|
+
.where(whereClause);
|
|
69
|
+
|
|
70
|
+
return ok({
|
|
71
|
+
rows: rows as PostSchema[],
|
|
72
|
+
total: totalRow?.count ?? 0,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async findBySlug(slug: string, excludeId?: string) {
|
|
78
|
+
return this.throwableAsync(async () => {
|
|
79
|
+
const whereClause = excludeId
|
|
80
|
+
? and(eq(this.table.slug, slug), ne(this.table.id, excludeId), isNull(this.table.deletedAt))
|
|
81
|
+
: and(eq(this.table.slug, slug), isNull(this.table.deletedAt));
|
|
82
|
+
|
|
83
|
+
const [row] = await this.orm.select().from(this.table).where(whereClause).limit(1);
|
|
84
|
+
return ok(row);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async resolveUniqueSlug(candidate: string, excludeId?: string) {
|
|
89
|
+
let slug = candidate;
|
|
90
|
+
let suffix = 2;
|
|
91
|
+
|
|
92
|
+
while (true) {
|
|
93
|
+
const existing = await this.findBySlug(slug, excludeId);
|
|
94
|
+
if (existing.isErr()) {
|
|
95
|
+
return existing;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!existing.value) {
|
|
99
|
+
return ok(slug);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
slug = `${candidate}-${suffix}`;
|
|
103
|
+
suffix += 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PostCreateInputSchema,
|
|
3
|
+
PostCreateOutputSchema,
|
|
4
|
+
PostPublishInputSchema,
|
|
5
|
+
PostPublishOutputSchema,
|
|
6
|
+
PostSoftDeleteInputSchema,
|
|
7
|
+
PostSoftDeleteOutputSchema,
|
|
8
|
+
PostsListInputSchema,
|
|
9
|
+
PostsListOutputSchema,
|
|
10
|
+
PostUpdateInputSchema,
|
|
11
|
+
PostUpdateOutputSchema,
|
|
12
|
+
} from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.schema";
|
|
13
|
+
import { BaseService } from "@m5kdev/backend/modules/base/base.service";
|
|
14
|
+
import type { ServerResultAsync } from "@m5kdev/backend/utils/types";
|
|
15
|
+
import { err, ok } from "neverthrow";
|
|
16
|
+
import type { PostsRepository } from "./posts.repository";
|
|
17
|
+
|
|
18
|
+
type RequestContext = {
|
|
19
|
+
session: {
|
|
20
|
+
activeOrganizationId?: string | null;
|
|
21
|
+
activeTeamId?: string | null;
|
|
22
|
+
};
|
|
23
|
+
user: {
|
|
24
|
+
id: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export class PostsService extends BaseService<{ posts: PostsRepository }, Record<string, never>> {
|
|
29
|
+
async list(
|
|
30
|
+
input: PostsListInputSchema,
|
|
31
|
+
_ctx: RequestContext
|
|
32
|
+
): ServerResultAsync<PostsListOutputSchema> {
|
|
33
|
+
return this.repository.posts.list(input);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async create(
|
|
37
|
+
input: PostCreateInputSchema,
|
|
38
|
+
ctx: RequestContext
|
|
39
|
+
): ServerResultAsync<PostCreateOutputSchema> {
|
|
40
|
+
const uniqueSlug = await this.repository.posts.resolveUniqueSlug(
|
|
41
|
+
this.slugify(input.slug ?? input.title)
|
|
42
|
+
);
|
|
43
|
+
if (uniqueSlug.isErr()) {
|
|
44
|
+
return err(uniqueSlug.error);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return this.repository.posts.create({
|
|
48
|
+
authorUserId: ctx.user.id,
|
|
49
|
+
organizationId: ctx.session.activeOrganizationId ?? null,
|
|
50
|
+
teamId: ctx.session.activeTeamId ?? null,
|
|
51
|
+
title: input.title.trim(),
|
|
52
|
+
slug: uniqueSlug.value,
|
|
53
|
+
excerpt: this.createExcerpt(input.excerpt, input.content),
|
|
54
|
+
content: input.content.trim(),
|
|
55
|
+
status: "draft",
|
|
56
|
+
}) as ServerResultAsync<PostCreateOutputSchema>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async update(
|
|
60
|
+
input: PostUpdateInputSchema,
|
|
61
|
+
_ctx: RequestContext
|
|
62
|
+
): ServerResultAsync<PostUpdateOutputSchema> {
|
|
63
|
+
const current = await this.repository.posts.findById(input.id);
|
|
64
|
+
if (current.isErr()) {
|
|
65
|
+
return err(current.error);
|
|
66
|
+
}
|
|
67
|
+
if (!current.value || current.value.deletedAt) {
|
|
68
|
+
return this.error("NOT_FOUND", "Post not found");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const uniqueSlug = await this.repository.posts.resolveUniqueSlug(
|
|
72
|
+
this.slugify(input.slug ?? input.title),
|
|
73
|
+
input.id
|
|
74
|
+
);
|
|
75
|
+
if (uniqueSlug.isErr()) {
|
|
76
|
+
return err(uniqueSlug.error);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return this.repository.posts.update({
|
|
80
|
+
id: input.id,
|
|
81
|
+
title: input.title.trim(),
|
|
82
|
+
slug: uniqueSlug.value,
|
|
83
|
+
excerpt: this.createExcerpt(input.excerpt, input.content),
|
|
84
|
+
content: input.content.trim(),
|
|
85
|
+
}) as ServerResultAsync<PostUpdateOutputSchema>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async publish(
|
|
89
|
+
input: PostPublishInputSchema,
|
|
90
|
+
_ctx: RequestContext
|
|
91
|
+
): ServerResultAsync<PostPublishOutputSchema> {
|
|
92
|
+
const current = await this.repository.posts.findById(input.id);
|
|
93
|
+
if (current.isErr()) {
|
|
94
|
+
return err(current.error);
|
|
95
|
+
}
|
|
96
|
+
if (!current.value || current.value.deletedAt) {
|
|
97
|
+
return this.error("NOT_FOUND", "Post not found");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return this.repository.posts.update({
|
|
101
|
+
id: input.id,
|
|
102
|
+
status: "published",
|
|
103
|
+
publishedAt: current.value.publishedAt ?? new Date(),
|
|
104
|
+
}) as ServerResultAsync<PostPublishOutputSchema>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async softDelete(
|
|
108
|
+
input: PostSoftDeleteInputSchema,
|
|
109
|
+
_ctx: RequestContext
|
|
110
|
+
): ServerResultAsync<PostSoftDeleteOutputSchema> {
|
|
111
|
+
const current = await this.repository.posts.findById(input.id);
|
|
112
|
+
if (current.isErr()) {
|
|
113
|
+
return err(current.error);
|
|
114
|
+
}
|
|
115
|
+
if (!current.value || current.value.deletedAt) {
|
|
116
|
+
return this.error("NOT_FOUND", "Post not found");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const updated = await this.repository.posts.update({
|
|
120
|
+
id: input.id,
|
|
121
|
+
deletedAt: new Date(),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (updated.isErr()) {
|
|
125
|
+
return err(updated.error);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return ok({ id: updated.value.id });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private slugify(value: string): string {
|
|
132
|
+
const slug = value
|
|
133
|
+
.trim()
|
|
134
|
+
.toLowerCase()
|
|
135
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
136
|
+
.replace(/^-+/g, "")
|
|
137
|
+
.replace(/-+$/g, "")
|
|
138
|
+
.replace(/-{2,}/g, "-");
|
|
139
|
+
|
|
140
|
+
return slug || "post";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private createExcerpt(excerpt: string | undefined, content: string): string {
|
|
144
|
+
if (excerpt?.trim()) {
|
|
145
|
+
return excerpt.trim();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return content.replace(/\s+/g, " ").trim().slice(0, 180);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
postCreateInputSchema,
|
|
3
|
+
postCreateOutputSchema,
|
|
4
|
+
postPublishInputSchema,
|
|
5
|
+
postPublishOutputSchema,
|
|
6
|
+
postSoftDeleteInputSchema,
|
|
7
|
+
postSoftDeleteOutputSchema,
|
|
8
|
+
postsListInputSchema,
|
|
9
|
+
postsListOutputSchema,
|
|
10
|
+
postUpdateInputSchema,
|
|
11
|
+
postUpdateOutputSchema,
|
|
12
|
+
} from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.schema";
|
|
13
|
+
import { handleTRPCResult } from "@m5kdev/backend/utils/trpc";
|
|
14
|
+
import { postsService } from "../../service";
|
|
15
|
+
import { procedure, router } from "../../utils/trpc";
|
|
16
|
+
|
|
17
|
+
export const postsRouter = router({
|
|
18
|
+
list: procedure
|
|
19
|
+
.input(postsListInputSchema)
|
|
20
|
+
.output(postsListOutputSchema)
|
|
21
|
+
.query(async ({ ctx, input }) => handleTRPCResult(await postsService.list(input ?? {}, ctx))),
|
|
22
|
+
|
|
23
|
+
create: procedure
|
|
24
|
+
.input(postCreateInputSchema)
|
|
25
|
+
.output(postCreateOutputSchema)
|
|
26
|
+
.mutation(async ({ ctx, input }) => handleTRPCResult(await postsService.create(input, ctx))),
|
|
27
|
+
|
|
28
|
+
update: procedure
|
|
29
|
+
.input(postUpdateInputSchema)
|
|
30
|
+
.output(postUpdateOutputSchema)
|
|
31
|
+
.mutation(async ({ ctx, input }) => handleTRPCResult(await postsService.update(input, ctx))),
|
|
32
|
+
|
|
33
|
+
publish: procedure
|
|
34
|
+
.input(postPublishInputSchema)
|
|
35
|
+
.output(postPublishOutputSchema)
|
|
36
|
+
.mutation(async ({ ctx, input }) => handleTRPCResult(await postsService.publish(input, ctx))),
|
|
37
|
+
|
|
38
|
+
softDelete: procedure
|
|
39
|
+
.input(postSoftDeleteInputSchema)
|
|
40
|
+
.output(postSoftDeleteOutputSchema)
|
|
41
|
+
.mutation(async ({ ctx, input }) =>
|
|
42
|
+
handleTRPCResult(await postsService.softDelete(input, ctx))
|
|
43
|
+
),
|
|
44
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AuthRepository } from "@m5kdev/backend/modules/auth/auth.repository";
|
|
2
|
+
import { orm, schema } from "./db";
|
|
3
|
+
import { PostsRepository } from "./modules/posts/posts.repository";
|
|
4
|
+
|
|
5
|
+
export const authRepository = new AuthRepository({ orm, schema });
|
|
6
|
+
export const postsRepository = new PostsRepository({ orm, schema, table: schema.posts });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AuthService } from "@m5kdev/backend/modules/auth/auth.service";
|
|
2
|
+
import { LocalEmailService } from "./lib/localEmailService";
|
|
3
|
+
import { PostsService } from "./modules/posts/posts.service";
|
|
4
|
+
import { authRepository, postsRepository } from "./repository";
|
|
5
|
+
|
|
6
|
+
export const emailService = new LocalEmailService({
|
|
7
|
+
appName: "{{APP_NAME}}",
|
|
8
|
+
appUrl: process.env.VITE_APP_URL ?? "http://localhost:5173",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const authService = new AuthService({ auth: authRepository }, { email: emailService });
|
|
12
|
+
export const postsService = new PostsService({ posts: postsRepository }, {});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
|
2
|
+
import { postsRouter as posts } from "./modules/posts/posts.trpc";
|
|
3
|
+
import { router } from "./utils/trpc";
|
|
4
|
+
|
|
5
|
+
export const appRouter = router({
|
|
6
|
+
posts,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export type AppRouter = typeof appRouter;
|
|
10
|
+
export type RouterInputs = inferRouterInputs<AppRouter>;
|
|
11
|
+
export type RouterOutputs = inferRouterOutputs<AppRouter>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type createAuthContext,
|
|
3
|
+
verifyProtectedProcedureContext,
|
|
4
|
+
} from "@m5kdev/backend/utils/trpc";
|
|
5
|
+
import { transformer } from "@m5kdev/commons/utils/trpc";
|
|
6
|
+
import { initTRPC } from "@trpc/server";
|
|
7
|
+
|
|
8
|
+
type Context = Awaited<ReturnType<ReturnType<typeof createAuthContext>>>;
|
|
9
|
+
|
|
10
|
+
const t = initTRPC.context<Context>().create({ transformer });
|
|
11
|
+
|
|
12
|
+
export const publicProcedure = t.procedure;
|
|
13
|
+
|
|
14
|
+
export const procedure = t.procedure.use(({ ctx, next }) => {
|
|
15
|
+
verifyProtectedProcedureContext(ctx);
|
|
16
|
+
return next({ ctx });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const router = t.router;
|
|
20
|
+
export const mergeRouters = t.mergeRouters;
|
|
21
|
+
|
|
22
|
+
export const trpcObject = {
|
|
23
|
+
router,
|
|
24
|
+
privateProcedure: procedure,
|
|
25
|
+
adminProcedure: procedure,
|
|
26
|
+
publicProcedure,
|
|
27
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@m5kdev/config/tsconfig.node.json",
|
|
3
|
+
"rootDir": ".",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"esModuleInterop": true,
|
|
6
|
+
"importHelpers": true,
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"tsBuildInfoFile": "dist/tsconfig.tsbuildinfo"
|
|
10
|
+
},
|
|
11
|
+
"exclude": ["node_modules"],
|
|
12
|
+
"include": ["src/**/*.ts", "drizzle/**/*.ts", "drizzle.config.ts", "tsup.config.ts"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Auth
|
|
2
|
+
BETTER_AUTH_SECRET=replace-me
|
|
3
|
+
|
|
4
|
+
# App URLs
|
|
5
|
+
VITE_APP_NAME={{APP_NAME}}
|
|
6
|
+
VITE_APP_URL=http://localhost:5173
|
|
7
|
+
VITE_SERVER_URL=http://localhost:8080
|
|
8
|
+
|
|
9
|
+
# Database
|
|
10
|
+
DATABASE_URL=file:./local.db
|
|
11
|
+
TURSO_DATABASE_URL=
|
|
12
|
+
TURSO_AUTH_TOKEN=
|
|
13
|
+
|
|
14
|
+
# Optional email and analytics providers
|
|
15
|
+
RESEND_API_KEY=
|
|
16
|
+
VITE_PUBLIC_POSTHOG_KEY=demo
|
|
17
|
+
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Auth
|
|
2
|
+
BETTER_AUTH_SECRET={{BETTER_AUTH_SECRET}}
|
|
3
|
+
|
|
4
|
+
# App URLs
|
|
5
|
+
VITE_APP_NAME={{APP_NAME}}
|
|
6
|
+
VITE_APP_URL=http://localhost:5173
|
|
7
|
+
VITE_SERVER_URL=http://localhost:8080
|
|
8
|
+
|
|
9
|
+
# Database
|
|
10
|
+
DATABASE_URL=file:./local.db
|
|
11
|
+
TURSO_DATABASE_URL=
|
|
12
|
+
TURSO_AUTH_TOKEN=
|
|
13
|
+
|
|
14
|
+
# Local email delivery falls back to file output when unset
|
|
15
|
+
RESEND_API_KEY=
|
|
16
|
+
|
|
17
|
+
# Optional PostHog keys
|
|
18
|
+
VITE_PUBLIC_POSTHOG_KEY=demo
|
|
19
|
+
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PACKAGE_SCOPE}}/shared",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc --noEmit",
|
|
7
|
+
"lint": "biome check .",
|
|
8
|
+
"lint:fix": "biome check . --write",
|
|
9
|
+
"check-types": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@m5kdev/commons": "catalog:",
|
|
13
|
+
"zod": "catalog:"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@m5kdev/config": "catalog:",
|
|
17
|
+
"tslib": "catalog:",
|
|
18
|
+
"typescript": "catalog:"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
"./modules/app/*": "./src/modules/app/*.ts",
|
|
22
|
+
"./modules/posts/*": "./src/modules/posts/*.ts"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { querySchema } from "@m5kdev/commons/modules/schemas/query.schema";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { POST_STATUS_VALUES } from "./posts.constants";
|
|
4
|
+
|
|
5
|
+
export const postStatusSchema = z.enum(POST_STATUS_VALUES);
|
|
6
|
+
export type PostStatusSchema = z.infer<typeof postStatusSchema>;
|
|
7
|
+
|
|
8
|
+
export const postSchema = z.object({
|
|
9
|
+
id: z.string(),
|
|
10
|
+
authorUserId: z.string().nullish(),
|
|
11
|
+
organizationId: z.string().nullish(),
|
|
12
|
+
teamId: z.string().nullish(),
|
|
13
|
+
title: z.string(),
|
|
14
|
+
slug: z.string(),
|
|
15
|
+
excerpt: z.string().nullish(),
|
|
16
|
+
content: z.string(),
|
|
17
|
+
status: postStatusSchema,
|
|
18
|
+
publishedAt: z.date().nullish(),
|
|
19
|
+
createdAt: z.date(),
|
|
20
|
+
updatedAt: z.date().nullish(),
|
|
21
|
+
deletedAt: z.date().nullish(),
|
|
22
|
+
});
|
|
23
|
+
export type PostSchema = z.infer<typeof postSchema>;
|
|
24
|
+
|
|
25
|
+
export const postsListInputSchema = querySchema.extend({
|
|
26
|
+
search: z.string().optional(),
|
|
27
|
+
status: postStatusSchema.optional(),
|
|
28
|
+
});
|
|
29
|
+
export type PostsListInputSchema = z.infer<typeof postsListInputSchema>;
|
|
30
|
+
|
|
31
|
+
export const postsListOutputSchema = z.object({
|
|
32
|
+
rows: z.array(postSchema),
|
|
33
|
+
total: z.number(),
|
|
34
|
+
});
|
|
35
|
+
export type PostsListOutputSchema = z.infer<typeof postsListOutputSchema>;
|
|
36
|
+
|
|
37
|
+
export const postCreateInputSchema = z.object({
|
|
38
|
+
title: z.string().min(1),
|
|
39
|
+
slug: z.string().optional(),
|
|
40
|
+
excerpt: z.string().optional(),
|
|
41
|
+
content: z.string().min(1),
|
|
42
|
+
});
|
|
43
|
+
export type PostCreateInputSchema = z.infer<typeof postCreateInputSchema>;
|
|
44
|
+
|
|
45
|
+
export const postCreateOutputSchema = postSchema;
|
|
46
|
+
export type PostCreateOutputSchema = z.infer<typeof postCreateOutputSchema>;
|
|
47
|
+
|
|
48
|
+
export const postUpdateInputSchema = z.object({
|
|
49
|
+
id: z.string(),
|
|
50
|
+
title: z.string().min(1),
|
|
51
|
+
slug: z.string().optional(),
|
|
52
|
+
excerpt: z.string().optional(),
|
|
53
|
+
content: z.string().min(1),
|
|
54
|
+
});
|
|
55
|
+
export type PostUpdateInputSchema = z.infer<typeof postUpdateInputSchema>;
|
|
56
|
+
|
|
57
|
+
export const postUpdateOutputSchema = postSchema;
|
|
58
|
+
export type PostUpdateOutputSchema = z.infer<typeof postUpdateOutputSchema>;
|
|
59
|
+
|
|
60
|
+
export const postPublishInputSchema = z.object({
|
|
61
|
+
id: z.string(),
|
|
62
|
+
});
|
|
63
|
+
export type PostPublishInputSchema = z.infer<typeof postPublishInputSchema>;
|
|
64
|
+
|
|
65
|
+
export const postPublishOutputSchema = postSchema;
|
|
66
|
+
export type PostPublishOutputSchema = z.infer<typeof postPublishOutputSchema>;
|
|
67
|
+
|
|
68
|
+
export const postSoftDeleteInputSchema = z.object({
|
|
69
|
+
id: z.string(),
|
|
70
|
+
});
|
|
71
|
+
export type PostSoftDeleteInputSchema = z.infer<typeof postSoftDeleteInputSchema>;
|
|
72
|
+
|
|
73
|
+
export const postSoftDeleteOutputSchema = z.object({
|
|
74
|
+
id: z.string(),
|
|
75
|
+
});
|
|
76
|
+
export type PostSoftDeleteOutputSchema = z.infer<typeof postSoftDeleteOutputSchema>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@m5kdev/config/tsconfig.base.json",
|
|
3
|
+
"rootDir": ".",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"esModuleInterop": true,
|
|
6
|
+
"importHelpers": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"tsBuildInfoFile": "dist/tsconfig.tsbuildinfo"
|
|
9
|
+
},
|
|
10
|
+
"exclude": ["node_modules"],
|
|
11
|
+
"include": ["src/**/*.ts"]
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## Frontend Stack
|
|
4
|
+
|
|
5
|
+
- React + TypeScript
|
|
6
|
+
- React Router v7
|
|
7
|
+
- HeroUI
|
|
8
|
+
- Tailwind CSS v4
|
|
9
|
+
- `nuqs` for URL state
|
|
10
|
+
- TanStack Query + tRPC
|
|
11
|
+
|
|
12
|
+
## Conventions
|
|
13
|
+
|
|
14
|
+
- Keep providers centralized in `src/Providers.tsx`.
|
|
15
|
+
- Keep routing in `src/Router.tsx`.
|
|
16
|
+
- Keep feature code grouped under `src/modules/<feature>/`.
|
|
17
|
+
- Prefer HeroUI primitives and existing framework utilities before adding custom abstractions.
|
|
18
|
+
- Use `useTranslation` for local app copy and rely on the bundled `web-ui` translations for auth routes.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta
|
|
6
|
+
name="viewport"
|
|
7
|
+
content="width=device-width, initial-scale=1.0"
|
|
8
|
+
/>
|
|
9
|
+
<meta
|
|
10
|
+
name="description"
|
|
11
|
+
content="{{APP_DESCRIPTION}}"
|
|
12
|
+
/>
|
|
13
|
+
<title>{{APP_NAME}}</title>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<div id="root"></div>
|
|
17
|
+
<script
|
|
18
|
+
type="module"
|
|
19
|
+
src="/src/main.tsx"
|
|
20
|
+
></script>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PACKAGE_SCOPE}}/webapp",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"lint": "biome check .",
|
|
10
|
+
"lint:fix": "biome check . --write",
|
|
11
|
+
"check-types": "tsc --noEmit",
|
|
12
|
+
"preview": "vite preview"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"{{PACKAGE_SCOPE}}/shared": "workspace:*",
|
|
16
|
+
"@heroui/react": "catalog:",
|
|
17
|
+
"@m5kdev/commons": "catalog:",
|
|
18
|
+
"@m5kdev/frontend": "catalog:",
|
|
19
|
+
"@m5kdev/web-ui": "catalog:",
|
|
20
|
+
"@tanstack/react-query": "catalog:",
|
|
21
|
+
"@tanstack/react-query-devtools": "catalog:",
|
|
22
|
+
"@trpc/client": "catalog:",
|
|
23
|
+
"@trpc/tanstack-react-query": "catalog:",
|
|
24
|
+
"better-auth": "catalog:",
|
|
25
|
+
"i18next": "catalog:",
|
|
26
|
+
"lucide-react": "catalog:",
|
|
27
|
+
"nuqs": "catalog:",
|
|
28
|
+
"posthog-js": "catalog:",
|
|
29
|
+
"react": "catalog:",
|
|
30
|
+
"react-dom": "catalog:",
|
|
31
|
+
"react-hook-form": "catalog:",
|
|
32
|
+
"react-i18next": "catalog:",
|
|
33
|
+
"react-router": "catalog:",
|
|
34
|
+
"sonner": "catalog:",
|
|
35
|
+
"tailwindcss": "catalog:",
|
|
36
|
+
"tw-animate-css": "catalog:",
|
|
37
|
+
"zod": "catalog:"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"{{PACKAGE_SCOPE}}/server": "workspace:*",
|
|
41
|
+
"@m5kdev/config": "catalog:",
|
|
42
|
+
"@tailwindcss/vite": "catalog:",
|
|
43
|
+
"@types/react": "catalog:",
|
|
44
|
+
"@types/react-dom": "catalog:",
|
|
45
|
+
"@vitejs/plugin-react": "catalog:",
|
|
46
|
+
"babel-plugin-react-compiler": "catalog:",
|
|
47
|
+
"typescript": "catalog:",
|
|
48
|
+
"vite": "catalog:",
|
|
49
|
+
"vite-plugin-i18next-loader": "catalog:",
|
|
50
|
+
"vite-tsconfig-paths": "catalog:"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NuqsAdapter } from "nuqs/adapters/react-router/v7";
|
|
2
|
+
import { BrowserRouter } from "react-router";
|
|
3
|
+
import { Providers } from "./Providers";
|
|
4
|
+
|
|
5
|
+
export function App() {
|
|
6
|
+
return (
|
|
7
|
+
<NuqsAdapter>
|
|
8
|
+
<BrowserRouter>
|
|
9
|
+
<Providers />
|
|
10
|
+
</BrowserRouter>
|
|
11
|
+
</NuqsAdapter>
|
|
12
|
+
);
|
|
13
|
+
}
|