create-m5kdev 0.30.0 → 0.31.1

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.
Files changed (60) hide show
  1. package/dist/src/catalog.cjs +140 -0
  2. package/dist/src/catalog.cjs.map +1 -0
  3. package/dist/src/catalog.d.cts +31 -0
  4. package/dist/src/changes.cjs +70 -0
  5. package/dist/src/changes.cjs.map +1 -0
  6. package/dist/src/changes.d.cts +26 -0
  7. package/dist/src/create.cjs +37 -23
  8. package/dist/src/create.cjs.map +1 -1
  9. package/dist/src/diagnostics.cjs +33 -0
  10. package/dist/src/diagnostics.cjs.map +1 -0
  11. package/dist/src/diagnostics.d.cts +20 -0
  12. package/dist/src/doctor.cjs +246 -0
  13. package/dist/src/doctor.cjs.map +1 -0
  14. package/dist/src/doctor.d.cts +13 -0
  15. package/dist/src/fs.cjs +56 -26
  16. package/dist/src/fs.cjs.map +1 -1
  17. package/dist/src/fs.d.cts +11 -4
  18. package/dist/src/init.cjs +110 -0
  19. package/dist/src/init.cjs.map +1 -0
  20. package/dist/src/init.d.cts +36 -0
  21. package/dist/src/migrations/ast.cjs +49 -0
  22. package/dist/src/migrations/ast.cjs.map +1 -0
  23. package/dist/src/migrations/ast.d.cts +10 -0
  24. package/dist/src/migrations/registry.cjs +41 -0
  25. package/dist/src/migrations/registry.cjs.map +1 -0
  26. package/dist/src/migrations/registry.d.cts +12 -0
  27. package/dist/src/migrations/types.cjs +0 -0
  28. package/dist/src/migrations/types.d.cts +22 -0
  29. package/dist/src/prompts.cjs +1 -1
  30. package/dist/src/prompts.cjs.map +1 -1
  31. package/dist/src/reconcile.cjs +361 -0
  32. package/dist/src/reconcile.cjs.map +1 -0
  33. package/dist/src/reconcile.d.cts +26 -0
  34. package/dist/src/runCli.cjs +155 -27
  35. package/dist/src/runCli.cjs.map +1 -1
  36. package/dist/src/state.cjs +107 -0
  37. package/dist/src/state.cjs.map +1 -0
  38. package/dist/src/state.d.cts +50 -0
  39. package/dist/src/template.cjs +45 -0
  40. package/dist/src/template.cjs.map +1 -0
  41. package/dist/src/template.d.cts +10 -0
  42. package/dist/src/types.d.cts +20 -1
  43. package/dist/src/update.cjs +91 -0
  44. package/dist/src/update.cjs.map +1 -0
  45. package/dist/src/update.d.cts +33 -0
  46. package/package.json +12 -4
  47. package/templates/minimal-app/README.md.tpl +18 -3
  48. package/templates/minimal-app/apps/email/src/emails/accountDeletionEmail.tsx.tpl +36 -35
  49. package/templates/minimal-app/apps/email/src/emails/organizationInviteEmail.tsx.tpl +35 -34
  50. package/templates/minimal-app/apps/email/src/emails/passwordResetEmail.tsx.tpl +36 -35
  51. package/templates/minimal-app/apps/email/src/emails/verificationEmail.tsx.tpl +38 -37
  52. package/templates/minimal-app/apps/email/src/emails/waitlistEmails.tsx.tpl +88 -87
  53. package/templates/minimal-app/apps/email/src/index.ts.tpl +74 -72
  54. package/templates/minimal-app/apps/email/tsconfig.json.tpl +11 -16
  55. package/templates/minimal-app/apps/server/tsconfig.json.tpl +13 -20
  56. package/templates/minimal-app/apps/shared/tsconfig.json.tpl +12 -16
  57. package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsList.ts.tpl +98 -98
  58. package/templates/minimal-app/biome.json.tpl +9 -2
  59. package/templates/minimal-app/pnpm-workspace.yaml.tpl +61 -63
  60. package/templates/minimal-app/template.manifest.json +30 -0
@@ -1,87 +1,88 @@
1
- /** @jsxRuntime automatic */
2
- import { Heading, Text } from "@react-email/components";
3
- import { CtaButton } from "@m5kdev/email/components/CtaButton";
4
- import { EmailLayout } from "@m5kdev/email/components/EmailLayout";
5
- import type { Brand, EmailTranslateFn } from "@m5kdev/email/types";
6
-
7
- function resolveT(t?: EmailTranslateFn): EmailTranslateFn {
8
- return t ?? ((key: string) => key);
9
- }
10
-
11
- interface WaitlistEmailProps {
12
- previewText: string;
13
- url?: string;
14
- brand: Brand;
15
- t?: EmailTranslateFn;
16
- htmlLang?: string;
17
- }
18
-
19
- function WaitlistActionEmail({
20
- previewText,
21
- url,
22
- brand,
23
- htmlLang,
24
- title,
25
- body,
26
- action,
27
- }: WaitlistEmailProps & { title: string; body: string; action: string }) {
28
- return (
29
- <EmailLayout previewText={previewText} brand={brand} htmlLang={htmlLang}>
30
- <Heading className="mb-4 text-2xl font-bold text-black">{title}</Heading>
31
- <Text className="mb-6 text-base text-gray-700">{body}</Text>
32
- {url ? <CtaButton href={url}>{action}</CtaButton> : null}
33
- </EmailLayout>
34
- );
35
- }
36
-
37
- export function WaitlistConfirmationEmail(props: WaitlistEmailProps) {
38
- const t = resolveT(props.t);
39
- return (
40
- <WaitlistActionEmail
41
- {...props}
42
- title={t("waitlistConfirmation.title")}
43
- body={t("waitlistConfirmation.body")}
44
- action={t("waitlistConfirmation.action")}
45
- />
46
- );
47
- }
48
-
49
- export function WaitlistInviteEmail(props: WaitlistEmailProps) {
50
- const t = resolveT(props.t);
51
- return (
52
- <WaitlistActionEmail
53
- {...props}
54
- title={t("waitlistInvite.title")}
55
- body={t("waitlistInvite.body")}
56
- action={t("waitlistInvite.action")}
57
- />
58
- );
59
- }
60
-
61
- export function WaitlistUserInviteEmail(
62
- props: WaitlistEmailProps & { inviter?: string; name?: string }
63
- ) {
64
- const t = resolveT(props.t);
65
- const inviter = props.inviter ?? "A teammate";
66
- const invitee = props.name ? t("waitlistUserInvite.inviteeSuffix", { name: props.name }) : "";
67
- return (
68
- <WaitlistActionEmail
69
- {...props}
70
- title={t("waitlistUserInvite.title", { inviter, invitee })}
71
- body={t("waitlistUserInvite.body")}
72
- action={t("waitlistUserInvite.action")}
73
- />
74
- );
75
- }
76
-
77
- export function SystemWaitlistNotificationEmail(props: WaitlistEmailProps) {
78
- const t = resolveT(props.t);
79
- return (
80
- <WaitlistActionEmail
81
- {...props}
82
- title={t("systemWaitlistNotification.title")}
83
- body={t("systemWaitlistNotification.body")}
84
- action={t("systemWaitlistNotification.action")}
85
- />
86
- );
87
- }
1
+ /** @jsxRuntime automatic */
2
+
3
+ import { CtaButton } from "@m5kdev/email/components/CtaButton";
4
+ import { EmailLayout } from "@m5kdev/email/components/EmailLayout";
5
+ import type { Brand, EmailTranslateFn } from "@m5kdev/email/types";
6
+ import { Heading, Text } from "@react-email/components";
7
+
8
+ function resolveT(t?: EmailTranslateFn): EmailTranslateFn {
9
+ return t ?? ((key: string) => key);
10
+ }
11
+
12
+ interface WaitlistEmailProps {
13
+ previewText: string;
14
+ url?: string;
15
+ brand: Brand;
16
+ t?: EmailTranslateFn;
17
+ htmlLang?: string;
18
+ }
19
+
20
+ function WaitlistActionEmail({
21
+ previewText,
22
+ url,
23
+ brand,
24
+ htmlLang,
25
+ title,
26
+ body,
27
+ action,
28
+ }: WaitlistEmailProps & { title: string; body: string; action: string }) {
29
+ return (
30
+ <EmailLayout previewText={previewText} brand={brand} htmlLang={htmlLang}>
31
+ <Heading className="mb-4 text-2xl font-bold text-black">{title}</Heading>
32
+ <Text className="mb-6 text-base text-gray-700">{body}</Text>
33
+ {url ? <CtaButton href={url}>{action}</CtaButton> : null}
34
+ </EmailLayout>
35
+ );
36
+ }
37
+
38
+ export function WaitlistConfirmationEmail(props: WaitlistEmailProps) {
39
+ const t = resolveT(props.t);
40
+ return (
41
+ <WaitlistActionEmail
42
+ {...props}
43
+ title={t("waitlistConfirmation.title")}
44
+ body={t("waitlistConfirmation.body")}
45
+ action={t("waitlistConfirmation.action")}
46
+ />
47
+ );
48
+ }
49
+
50
+ export function WaitlistInviteEmail(props: WaitlistEmailProps) {
51
+ const t = resolveT(props.t);
52
+ return (
53
+ <WaitlistActionEmail
54
+ {...props}
55
+ title={t("waitlistInvite.title")}
56
+ body={t("waitlistInvite.body")}
57
+ action={t("waitlistInvite.action")}
58
+ />
59
+ );
60
+ }
61
+
62
+ export function WaitlistUserInviteEmail(
63
+ props: WaitlistEmailProps & { inviter?: string; name?: string }
64
+ ) {
65
+ const t = resolveT(props.t);
66
+ const inviter = props.inviter ?? "A teammate";
67
+ const invitee = props.name ? t("waitlistUserInvite.inviteeSuffix", { name: props.name }) : "";
68
+ return (
69
+ <WaitlistActionEmail
70
+ {...props}
71
+ title={t("waitlistUserInvite.title", { inviter, invitee })}
72
+ body={t("waitlistUserInvite.body")}
73
+ action={t("waitlistUserInvite.action")}
74
+ />
75
+ );
76
+ }
77
+
78
+ export function SystemWaitlistNotificationEmail(props: WaitlistEmailProps) {
79
+ const t = resolveT(props.t);
80
+ return (
81
+ <WaitlistActionEmail
82
+ {...props}
83
+ title={t("systemWaitlistNotification.title")}
84
+ body={t("systemWaitlistNotification.body")}
85
+ action={t("systemWaitlistNotification.action")}
86
+ />
87
+ );
88
+ }
@@ -1,72 +1,74 @@
1
- import type { FunctionComponent } from "react";
2
- import AccountDeletionEmail from "./emails/accountDeletionEmail";
3
- import OrganizationInviteEmail from "./emails/organizationInviteEmail";
4
- import PasswordResetEmail from "./emails/passwordResetEmail";
5
- import VerificationEmail from "./emails/verificationEmail";
6
- import {
7
- SystemWaitlistNotificationEmail,
8
- WaitlistConfirmationEmail,
9
- WaitlistInviteEmail,
10
- WaitlistUserInviteEmail,
11
- } from "./emails/waitlistEmails";
12
-
13
- /**
14
- * EmailService renders templates with untyped props (they come from send-time
15
- * data), so the registry erases each template's prop type. The props each
16
- * template receives are guaranteed by the corresponding EmailService.send*
17
- * method.
18
- */
19
- function asTemplate(component: FunctionComponent<never>): FunctionComponent<Record<string, unknown>> {
20
- return component as unknown as FunctionComponent<Record<string, unknown>>;
21
- }
22
-
23
- export const templates = {
24
- accountDeletion: {
25
- id: "account-deletion",
26
- subject: "accountDeletion.subject",
27
- previewText: "accountDeletion.previewText",
28
- react: asTemplate(AccountDeletionEmail),
29
- },
30
- passwordReset: {
31
- id: "password-reset",
32
- subject: "passwordReset.subject",
33
- previewText: "passwordReset.previewText",
34
- react: asTemplate(PasswordResetEmail),
35
- },
36
- verification: {
37
- id: "verification",
38
- subject: "verification.subject",
39
- previewText: "verification.previewText",
40
- react: asTemplate(VerificationEmail),
41
- },
42
- organizationInvite: {
43
- id: "organization-invite",
44
- subject: "organizationInvite.subject",
45
- previewText: "organizationInvite.previewText",
46
- react: asTemplate(OrganizationInviteEmail),
47
- },
48
- waitlistConfirmation: {
49
- id: "waitlist-confirmation",
50
- subject: "waitlistConfirmation.subject",
51
- previewText: "waitlistConfirmation.previewText",
52
- react: asTemplate(WaitlistConfirmationEmail),
53
- },
54
- waitlistInvite: {
55
- id: "waitlist-invite",
56
- subject: "waitlistInvite.subject",
57
- previewText: "waitlistInvite.previewText",
58
- react: asTemplate(WaitlistInviteEmail),
59
- },
60
- waitlistUserInvite: {
61
- id: "waitlist-user-invite",
62
- subject: "waitlistUserInvite.subject",
63
- previewText: "waitlistUserInvite.previewText",
64
- react: asTemplate(WaitlistUserInviteEmail),
65
- },
66
- systemWaitlistNotification: {
67
- id: "system-waitlist-notification",
68
- subject: "systemWaitlistNotification.subject",
69
- previewText: "systemWaitlistNotification.previewText",
70
- react: asTemplate(SystemWaitlistNotificationEmail),
71
- },
72
- };
1
+ import type { FunctionComponent } from "react";
2
+ import AccountDeletionEmail from "./emails/accountDeletionEmail";
3
+ import OrganizationInviteEmail from "./emails/organizationInviteEmail";
4
+ import PasswordResetEmail from "./emails/passwordResetEmail";
5
+ import VerificationEmail from "./emails/verificationEmail";
6
+ import {
7
+ SystemWaitlistNotificationEmail,
8
+ WaitlistConfirmationEmail,
9
+ WaitlistInviteEmail,
10
+ WaitlistUserInviteEmail,
11
+ } from "./emails/waitlistEmails";
12
+
13
+ /**
14
+ * EmailService renders templates with untyped props (they come from send-time
15
+ * data), so the registry erases each template's prop type. The props each
16
+ * template receives are guaranteed by the corresponding EmailService.send*
17
+ * method.
18
+ */
19
+ function asTemplate(
20
+ component: FunctionComponent<never>
21
+ ): FunctionComponent<Record<string, unknown>> {
22
+ return component as unknown as FunctionComponent<Record<string, unknown>>;
23
+ }
24
+
25
+ export const templates = {
26
+ accountDeletion: {
27
+ id: "account-deletion",
28
+ subject: "accountDeletion.subject",
29
+ previewText: "accountDeletion.previewText",
30
+ react: asTemplate(AccountDeletionEmail),
31
+ },
32
+ passwordReset: {
33
+ id: "password-reset",
34
+ subject: "passwordReset.subject",
35
+ previewText: "passwordReset.previewText",
36
+ react: asTemplate(PasswordResetEmail),
37
+ },
38
+ verification: {
39
+ id: "verification",
40
+ subject: "verification.subject",
41
+ previewText: "verification.previewText",
42
+ react: asTemplate(VerificationEmail),
43
+ },
44
+ organizationInvite: {
45
+ id: "organization-invite",
46
+ subject: "organizationInvite.subject",
47
+ previewText: "organizationInvite.previewText",
48
+ react: asTemplate(OrganizationInviteEmail),
49
+ },
50
+ waitlistConfirmation: {
51
+ id: "waitlist-confirmation",
52
+ subject: "waitlistConfirmation.subject",
53
+ previewText: "waitlistConfirmation.previewText",
54
+ react: asTemplate(WaitlistConfirmationEmail),
55
+ },
56
+ waitlistInvite: {
57
+ id: "waitlist-invite",
58
+ subject: "waitlistInvite.subject",
59
+ previewText: "waitlistInvite.previewText",
60
+ react: asTemplate(WaitlistInviteEmail),
61
+ },
62
+ waitlistUserInvite: {
63
+ id: "waitlist-user-invite",
64
+ subject: "waitlistUserInvite.subject",
65
+ previewText: "waitlistUserInvite.previewText",
66
+ react: asTemplate(WaitlistUserInviteEmail),
67
+ },
68
+ systemWaitlistNotification: {
69
+ id: "system-waitlist-notification",
70
+ subject: "systemWaitlistNotification.subject",
71
+ previewText: "systemWaitlistNotification.previewText",
72
+ react: asTemplate(SystemWaitlistNotificationEmail),
73
+ },
74
+ };
@@ -1,16 +1,11 @@
1
- {
2
- "extends": "@m5kdev/config/tsconfig.node.json",
3
- "compilerOptions": {
4
- "jsx": "react-jsx",
5
- "noEmit": true,
6
- "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo",
7
- "rootDir": "."
8
- },
9
- "exclude": [
10
- "node_modules"
11
- ],
12
- "include": [
13
- "src/**/*.ts",
14
- "src/**/*.tsx"
15
- ]
16
- }
1
+ {
2
+ "extends": "@m5kdev/config/tsconfig.node.json",
3
+ "compilerOptions": {
4
+ "jsx": "react-jsx",
5
+ "noEmit": true,
6
+ "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo",
7
+ "rootDir": "."
8
+ },
9
+ "exclude": ["node_modules"],
10
+ "include": ["src/**/*.ts", "src/**/*.tsx"]
11
+ }
@@ -1,20 +1,13 @@
1
- {
2
- "extends": "@m5kdev/config/tsconfig.node.json",
3
- "compilerOptions": {
4
- "esModuleInterop": true,
5
- "importHelpers": true,
6
- "jsx": "react-jsx",
7
- "noEmit": true,
8
- "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo",
9
- "rootDir": "."
10
- },
11
- "exclude": [
12
- "node_modules"
13
- ],
14
- "include": [
15
- "src/**/*.ts",
16
- "drizzle/**/*.ts",
17
- "drizzle.config.ts",
18
- "tsup.config.ts"
19
- ]
20
- }
1
+ {
2
+ "extends": "@m5kdev/config/tsconfig.node.json",
3
+ "compilerOptions": {
4
+ "esModuleInterop": true,
5
+ "importHelpers": true,
6
+ "jsx": "react-jsx",
7
+ "noEmit": true,
8
+ "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo",
9
+ "rootDir": "."
10
+ },
11
+ "exclude": ["node_modules"],
12
+ "include": ["src/**/*.ts", "drizzle/**/*.ts", "drizzle.config.ts", "tsup.config.ts"]
13
+ }
@@ -1,16 +1,12 @@
1
- {
2
- "extends": "@m5kdev/config/tsconfig.base.json",
3
- "compilerOptions": {
4
- "esModuleInterop": true,
5
- "importHelpers": true,
6
- "noEmit": true,
7
- "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo",
8
- "rootDir": "."
9
- },
10
- "exclude": [
11
- "node_modules"
12
- ],
13
- "include": [
14
- "src/**/*.ts"
15
- ]
16
- }
1
+ {
2
+ "extends": "@m5kdev/config/tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "esModuleInterop": true,
5
+ "importHelpers": true,
6
+ "noEmit": true,
7
+ "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo",
8
+ "rootDir": "."
9
+ },
10
+ "exclude": ["node_modules"],
11
+ "include": ["src/**/*.ts"]
12
+ }
@@ -1,98 +1,98 @@
1
- import type { QueryInput } from "@m5kdev/commons/modules/schemas/query.schema";
2
- import {
3
- POST_FILTER_VALUES,
4
- POSTS_PAGE_SIZE,
5
- } from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.constants";
6
- import type { AppRouter } from "{{PACKAGE_SCOPE}}/server/types";
7
- import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
8
- import type { inferRouterOutputs } from "@trpc/server";
9
- import { parseAsInteger, parseAsString, parseAsStringLiteral, useQueryState } from "nuqs";
10
- import { startTransition, useDeferredValue, useMemo } from "react";
11
- import { useTRPC } from "@/utils/trpc";
12
-
13
- export type PostStatusFilter = (typeof POST_FILTER_VALUES)[number];
14
-
15
- // row/list types are inferred from the server router — no hand-written mirrors
16
- type PostsListOutput = inferRouterOutputs<AppRouter>["posts"]["list"];
17
- export type PostRow = PostsListOutput["rows"][number];
18
-
19
- const STATUS_PARSER = parseAsStringLiteral(POST_FILTER_VALUES).withDefault("all");
20
-
21
- /** List query plus its URL state (search, status filter, page) and derived stats. */
22
- export function usePostsList() {
23
- const trpc = useTRPC();
24
-
25
- const [search, setSearchParam] = useQueryState("search", parseAsString.withDefault(""));
26
- const [status, setStatusParam] = useQueryState<PostStatusFilter>("status", STATUS_PARSER);
27
- const [page, setPageParam] = useQueryState("page", parseAsInteger.withDefault(1));
28
-
29
- const deferredSearch = useDeferredValue(search);
30
-
31
- // the generic list contract: q for global search, filters for column filters
32
- const listInput = useMemo<QueryInput>(
33
- () => ({
34
- page,
35
- limit: POSTS_PAGE_SIZE,
36
- q: deferredSearch || undefined,
37
- filters:
38
- status === "all"
39
- ? undefined
40
- : [{ columnId: "status", type: "enum", method: "equals", value: status }],
41
- sort: "updatedAt",
42
- order: "desc",
43
- }),
44
- [deferredSearch, page, status]
45
- );
46
-
47
- const { data, isLoading, isFetching } = useQuery(
48
- trpc.posts.list.queryOptions(listInput) as unknown as UseQueryOptions<PostsListOutput>
49
- );
50
-
51
- const rows = data?.rows ?? [];
52
- const total = data?.total ?? 0;
53
- const pageCount = Math.max(1, Math.ceil(total / POSTS_PAGE_SIZE));
54
-
55
- const stats = useMemo(
56
- () => ({
57
- published: rows.filter((row) => row.status === "published").length,
58
- drafts: rows.filter((row) => row.status === "draft").length,
59
- total,
60
- }),
61
- [rows, total]
62
- );
63
-
64
- const setSearch = (value: string) => {
65
- startTransition(() => {
66
- void setSearchParam(value || null);
67
- void setPageParam(1);
68
- });
69
- };
70
-
71
- const setStatus = (value: PostStatusFilter) => {
72
- startTransition(() => {
73
- void setStatusParam(value);
74
- void setPageParam(1);
75
- });
76
- };
77
-
78
- const goToPage = (next: number) => {
79
- startTransition(() => {
80
- void setPageParam(Math.min(Math.max(1, next), pageCount));
81
- });
82
- };
83
-
84
- return {
85
- rows,
86
- total,
87
- pageCount,
88
- stats,
89
- page,
90
- search,
91
- status,
92
- isLoading,
93
- isFetching,
94
- setSearch,
95
- setStatus,
96
- goToPage,
97
- };
98
- }
1
+ import type { QueryInput } from "@m5kdev/commons/modules/schemas/query.schema";
2
+ import type { AppRouter } from "{{PACKAGE_SCOPE}}/server/types";
3
+ import {
4
+ POST_FILTER_VALUES,
5
+ POSTS_PAGE_SIZE,
6
+ } from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.constants";
7
+ import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
8
+ import type { inferRouterOutputs } from "@trpc/server";
9
+ import { parseAsInteger, parseAsString, parseAsStringLiteral, useQueryState } from "nuqs";
10
+ import { startTransition, useDeferredValue, useMemo } from "react";
11
+ import { useTRPC } from "@/utils/trpc";
12
+
13
+ export type PostStatusFilter = (typeof POST_FILTER_VALUES)[number];
14
+
15
+ // row/list types are inferred from the server router — no hand-written mirrors
16
+ type PostsListOutput = inferRouterOutputs<AppRouter>["posts"]["list"];
17
+ export type PostRow = PostsListOutput["rows"][number];
18
+
19
+ const STATUS_PARSER = parseAsStringLiteral(POST_FILTER_VALUES).withDefault("all");
20
+
21
+ /** List query plus its URL state (search, status filter, page) and derived stats. */
22
+ export function usePostsList() {
23
+ const trpc = useTRPC();
24
+
25
+ const [search, setSearchParam] = useQueryState("search", parseAsString.withDefault(""));
26
+ const [status, setStatusParam] = useQueryState<PostStatusFilter>("status", STATUS_PARSER);
27
+ const [page, setPageParam] = useQueryState("page", parseAsInteger.withDefault(1));
28
+
29
+ const deferredSearch = useDeferredValue(search);
30
+
31
+ // the generic list contract: q for global search, filters for column filters
32
+ const listInput = useMemo<QueryInput>(
33
+ () => ({
34
+ page,
35
+ limit: POSTS_PAGE_SIZE,
36
+ q: deferredSearch || undefined,
37
+ filters:
38
+ status === "all"
39
+ ? undefined
40
+ : [{ columnId: "status", type: "enum", method: "equals", value: status }],
41
+ sort: "updatedAt",
42
+ order: "desc",
43
+ }),
44
+ [deferredSearch, page, status]
45
+ );
46
+
47
+ const { data, isLoading, isFetching } = useQuery(
48
+ trpc.posts.list.queryOptions(listInput) as unknown as UseQueryOptions<PostsListOutput>
49
+ );
50
+
51
+ const rows = data?.rows ?? [];
52
+ const total = data?.total ?? 0;
53
+ const pageCount = Math.max(1, Math.ceil(total / POSTS_PAGE_SIZE));
54
+
55
+ const stats = useMemo(
56
+ () => ({
57
+ published: rows.filter((row) => row.status === "published").length,
58
+ drafts: rows.filter((row) => row.status === "draft").length,
59
+ total,
60
+ }),
61
+ [rows, total]
62
+ );
63
+
64
+ const setSearch = (value: string) => {
65
+ startTransition(() => {
66
+ void setSearchParam(value || null);
67
+ void setPageParam(1);
68
+ });
69
+ };
70
+
71
+ const setStatus = (value: PostStatusFilter) => {
72
+ startTransition(() => {
73
+ void setStatusParam(value);
74
+ void setPageParam(1);
75
+ });
76
+ };
77
+
78
+ const goToPage = (next: number) => {
79
+ startTransition(() => {
80
+ void setPageParam(Math.min(Math.max(1, next), pageCount));
81
+ });
82
+ };
83
+
84
+ return {
85
+ rows,
86
+ total,
87
+ pageCount,
88
+ stats,
89
+ page,
90
+ search,
91
+ status,
92
+ isLoading,
93
+ isFetching,
94
+ setSearch,
95
+ setStatus,
96
+ goToPage,
97
+ };
98
+ }
@@ -7,14 +7,21 @@
7
7
  },
8
8
  "files": {
9
9
  "ignoreUnknown": false,
10
- "includes": ["**", "!**/node_modules/**", "!**/dist/**", "!**/.react-router/**"]
10
+ "includes": [
11
+ "**",
12
+ "!**/node_modules",
13
+ "!**/dist",
14
+ "!**/.react-router",
15
+ "!**/.m5kdev.json",
16
+ "!**/drizzle/meta"
17
+ ]
11
18
  },
12
19
  "formatter": {
13
20
  "enabled": true,
14
21
  "indentWidth": 2,
15
22
  "indentStyle": "space",
16
23
  "lineWidth": 100,
17
- "lineEnding": "crlf"
24
+ "lineEnding": "lf"
18
25
  },
19
26
  "linter": {
20
27
  "enabled": true,