create-m5kdev 0.29.0 → 0.31.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/catalog.cjs +139 -0
- package/dist/src/catalog.cjs.map +1 -0
- package/dist/src/catalog.d.cts +30 -0
- package/dist/src/changes.cjs +70 -0
- package/dist/src/changes.cjs.map +1 -0
- package/dist/src/changes.d.cts +26 -0
- package/dist/src/create.cjs +37 -23
- package/dist/src/create.cjs.map +1 -1
- package/dist/src/diagnostics.cjs +33 -0
- package/dist/src/diagnostics.cjs.map +1 -0
- package/dist/src/diagnostics.d.cts +20 -0
- package/dist/src/doctor.cjs +245 -0
- package/dist/src/doctor.cjs.map +1 -0
- package/dist/src/doctor.d.cts +13 -0
- package/dist/src/fs.cjs +56 -26
- package/dist/src/fs.cjs.map +1 -1
- package/dist/src/fs.d.cts +11 -4
- package/dist/src/init.cjs +110 -0
- package/dist/src/init.cjs.map +1 -0
- package/dist/src/init.d.cts +36 -0
- package/dist/src/migrations/ast.cjs +49 -0
- package/dist/src/migrations/ast.cjs.map +1 -0
- package/dist/src/migrations/ast.d.cts +10 -0
- package/dist/src/migrations/registry.cjs +41 -0
- package/dist/src/migrations/registry.cjs.map +1 -0
- package/dist/src/migrations/registry.d.cts +12 -0
- package/dist/src/migrations/types.cjs +0 -0
- package/dist/src/migrations/types.d.cts +22 -0
- package/dist/src/prompts.cjs +1 -1
- package/dist/src/prompts.cjs.map +1 -1
- package/dist/src/reconcile.cjs +361 -0
- package/dist/src/reconcile.cjs.map +1 -0
- package/dist/src/reconcile.d.cts +26 -0
- package/dist/src/runCli.cjs +155 -27
- package/dist/src/runCli.cjs.map +1 -1
- package/dist/src/state.cjs +104 -0
- package/dist/src/state.cjs.map +1 -0
- package/dist/src/state.d.cts +50 -0
- package/dist/src/template.cjs +45 -0
- package/dist/src/template.cjs.map +1 -0
- package/dist/src/template.d.cts +10 -0
- package/dist/src/types.d.cts +20 -1
- package/dist/src/update.cjs +91 -0
- package/dist/src/update.cjs.map +1 -0
- package/dist/src/update.d.cts +33 -0
- package/package.json +12 -4
- package/templates/minimal-app/README.md.tpl +18 -3
- package/templates/minimal-app/apps/email/src/emails/accountDeletionEmail.tsx.tpl +36 -35
- package/templates/minimal-app/apps/email/src/emails/organizationInviteEmail.tsx.tpl +35 -34
- package/templates/minimal-app/apps/email/src/emails/passwordResetEmail.tsx.tpl +36 -35
- package/templates/minimal-app/apps/email/src/emails/verificationEmail.tsx.tpl +38 -37
- package/templates/minimal-app/apps/email/src/emails/waitlistEmails.tsx.tpl +88 -87
- package/templates/minimal-app/apps/email/src/index.ts.tpl +74 -72
- package/templates/minimal-app/apps/email/tsconfig.json.tpl +11 -16
- package/templates/minimal-app/apps/server/tsconfig.json.tpl +13 -20
- package/templates/minimal-app/apps/shared/tsconfig.json.tpl +12 -16
- package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsList.ts.tpl +98 -98
- package/templates/minimal-app/biome.json.tpl +9 -2
- package/templates/minimal-app/pnpm-workspace.yaml.tpl +61 -63
- package/templates/minimal-app/template.manifest.json +30 -0
|
@@ -1,87 +1,88 @@
|
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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": [
|
|
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": "
|
|
24
|
+
"lineEnding": "lf"
|
|
18
25
|
},
|
|
19
26
|
"linter": {
|
|
20
27
|
"enabled": true,
|