create-m5kdev 0.21.6 → 0.23.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-m5kdev",
3
- "version": "0.21.6",
3
+ "version": "0.23.0",
4
4
  "license": "GPL-3.0-only",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,11 +10,14 @@
10
10
  "email:dev": "react-email dev --dir src/emails"
11
11
  },
12
12
  "dependencies": {
13
+ "@m5kdev/commons": "workspace:*",
14
+ "@m5kdev/email": "workspace:*",
13
15
  "@react-email/components": "catalog:",
14
16
  "react": "catalog:",
15
17
  "react-dom": "catalog:"
16
18
  },
17
19
  "devDependencies": {
20
+ "@m5kdev/backend": "workspace:*",
18
21
  "@m5kdev/config": "catalog:",
19
22
  "@types/node": "catalog:",
20
23
  "@types/react": "catalog:",
@@ -23,5 +26,9 @@
23
26
  "tslib": "catalog:",
24
27
  "typescript": "catalog:"
25
28
  },
26
- "main": "./src/index.ts"
29
+ "main": "./src/index.ts",
30
+ "exports": {
31
+ ".": "./src/index.ts",
32
+ "./resources": "./src/resources.ts"
33
+ }
27
34
  }
@@ -1,30 +1,34 @@
1
- import { Text } from "@react-email/components";
2
- import { BaseEmail } from "../components/BaseEmail";
1
+ import { Heading, Text } from "@react-email/components";
2
+ import { CtaButton } from "@m5kdev/email/components/CtaButton";
3
+ import { EmailLayout } from "@m5kdev/email/components/EmailLayout";
4
+ import type { Brand, EmailTranslateFn } from "@m5kdev/email/types";
5
+
6
+ function resolveT(t?: EmailTranslateFn): EmailTranslateFn {
7
+ return t ?? ((key: string) => key);
8
+ }
3
9
 
4
10
  export default function AccountDeletionEmail({
5
- previewText = "Confirm your account deletion",
11
+ previewText,
6
12
  url,
13
+ brand,
14
+ t,
15
+ htmlLang,
7
16
  }: {
8
- previewText?: string;
17
+ previewText: string;
9
18
  url: string;
19
+ brand: Brand;
20
+ t?: EmailTranslateFn;
21
+ htmlLang?: string;
10
22
  }) {
23
+ const translate = resolveT(t);
24
+
11
25
  return (
12
- <BaseEmail
13
- previewText={previewText}
14
- eyebrow="Account Safety"
15
- title="Confirm account deletion"
16
- ctaLabel="Review deletion"
17
- ctaUrl={url}
18
- body={
19
- <>
20
- <Text style={{ margin: "0" }}>
21
- We received a request to delete your {{APP_NAME}} account.
22
- </Text>
23
- <Text style={{ margin: "0" }}>
24
- If that was you, use the link below to confirm the action.
25
- </Text>
26
- </>
27
- }
28
- />
26
+ <EmailLayout previewText={previewText} brand={brand} htmlLang={htmlLang}>
27
+ <Heading className="mb-4 text-2xl font-bold text-black">
28
+ {translate("accountDeletion.title")}
29
+ </Heading>
30
+ <Text className="mb-6 text-base text-gray-700">{translate("accountDeletion.body")}</Text>
31
+ <CtaButton href={url}>{translate("accountDeletion.action")}</CtaButton>
32
+ </EmailLayout>
29
33
  );
30
34
  }
@@ -1,31 +1,33 @@
1
- import { Text } from "@react-email/components";
2
- import { BaseEmail } from "../components/BaseEmail";
1
+ import { Heading, Text } from "@react-email/components";
2
+ import { CtaButton } from "@m5kdev/email/components/CtaButton";
3
+ import { EmailLayout } from "@m5kdev/email/components/EmailLayout";
4
+ import type { EmailTranslateFn, OrganizationInviteTemplateProps } from "@m5kdev/email/types";
5
+
6
+ function resolveT(t?: EmailTranslateFn): EmailTranslateFn {
7
+ return t ?? ((key: string) => key);
8
+ }
3
9
 
4
10
  export default function OrganizationInviteEmail({
5
- previewText = "You have been invited to join an organization",
11
+ previewText,
6
12
  inviterName,
7
13
  organizationName,
8
14
  role,
9
15
  url,
10
- }: {
11
- previewText?: string;
12
- inviterName: string;
13
- organizationName: string;
14
- role: string;
15
- url: string;
16
- }) {
16
+ brand,
17
+ t,
18
+ htmlLang,
19
+ }: OrganizationInviteTemplateProps) {
20
+ const translate = resolveT(t);
21
+
17
22
  return (
18
- <BaseEmail
19
- previewText={previewText}
20
- eyebrow="Collaboration"
21
- title={`Join ${organizationName}`}
22
- ctaLabel="Accept invitation"
23
- ctaUrl={url}
24
- body={
25
- <Text style={{ margin: "0" }}>
26
- {inviterName} invited you to join {organizationName} as {role}.
27
- </Text>
28
- }
29
- />
23
+ <EmailLayout previewText={previewText} brand={brand} htmlLang={htmlLang}>
24
+ <Heading className="mb-4 text-2xl font-bold text-black">
25
+ {translate("organizationInvite.title", { organizationName })}
26
+ </Heading>
27
+ <Text className="mb-6 text-base text-gray-700">
28
+ {translate("organizationInvite.body", { inviterName, organizationName, role })}
29
+ </Text>
30
+ <CtaButton href={url}>{translate("organizationInvite.action")}</CtaButton>
31
+ </EmailLayout>
30
32
  );
31
33
  }
@@ -1,25 +1,34 @@
1
- import { Text } from "@react-email/components";
2
- import { BaseEmail } from "../components/BaseEmail";
1
+ import { Heading, Text } from "@react-email/components";
2
+ import { CtaButton } from "@m5kdev/email/components/CtaButton";
3
+ import { EmailLayout } from "@m5kdev/email/components/EmailLayout";
4
+ import type { Brand, EmailTranslateFn } from "@m5kdev/email/types";
5
+
6
+ function resolveT(t?: EmailTranslateFn): EmailTranslateFn {
7
+ return t ?? ((key: string) => key);
8
+ }
3
9
 
4
10
  export default function PasswordResetEmail({
5
- previewText = "Reset your password",
11
+ previewText,
6
12
  url,
13
+ brand,
14
+ t,
15
+ htmlLang,
7
16
  }: {
8
- previewText?: string;
17
+ previewText: string;
9
18
  url: string;
19
+ brand: Brand;
20
+ t?: EmailTranslateFn;
21
+ htmlLang?: string;
10
22
  }) {
23
+ const translate = resolveT(t);
24
+
11
25
  return (
12
- <BaseEmail
13
- previewText={previewText}
14
- eyebrow="Access"
15
- title="Reset your password"
16
- ctaLabel="Choose a new password"
17
- ctaUrl={url}
18
- body={
19
- <Text style={{ margin: "0" }}>
20
- Use the secure link below to choose a new password for your {{APP_NAME}} account.
21
- </Text>
22
- }
23
- />
26
+ <EmailLayout previewText={previewText} brand={brand} htmlLang={htmlLang}>
27
+ <Heading className="mb-4 text-2xl font-bold text-black">
28
+ {translate("passwordReset.title")}
29
+ </Heading>
30
+ <Text className="mb-6 text-base text-gray-700">{translate("passwordReset.body")}</Text>
31
+ <CtaButton href={url}>{translate("passwordReset.action")}</CtaButton>
32
+ </EmailLayout>
24
33
  );
25
34
  }
@@ -1,26 +1,36 @@
1
- import { Text } from "@react-email/components";
2
- import { BaseEmail } from "../components/BaseEmail";
1
+ import { Heading, Text } from "@react-email/components";
2
+ import { CtaButton } from "@m5kdev/email/components/CtaButton";
3
+ import { EmailLayout } from "@m5kdev/email/components/EmailLayout";
4
+ import type { Brand, EmailTranslateFn } from "@m5kdev/email/types";
5
+
6
+ function resolveT(t?: EmailTranslateFn): EmailTranslateFn {
7
+ return t ?? ((key: string) => key);
8
+ }
3
9
 
4
10
  export default function VerificationEmail({
5
- previewText = "Verify your email",
11
+ previewText,
6
12
  url,
13
+ brand,
14
+ t,
15
+ htmlLang,
7
16
  }: {
8
- previewText?: string;
17
+ previewText: string;
9
18
  url: string;
19
+ brand: Brand;
20
+ t?: EmailTranslateFn;
21
+ htmlLang?: string;
10
22
  }) {
23
+ const translate = resolveT(t);
24
+
11
25
  return (
12
- <BaseEmail
13
- previewText={previewText}
14
- eyebrow="Welcome"
15
- title="Verify your email"
16
- ctaLabel="Verify account"
17
- ctaUrl={url}
18
- body={
19
- <Text style={{ margin: "0" }}>
20
- Confirm your email address to finish setting up {{APP_NAME}} and access your editorial
21
- workspace.
22
- </Text>
23
- }
24
- />
26
+ <EmailLayout previewText={previewText} brand={brand} htmlLang={htmlLang}>
27
+ <Heading className="mb-4 text-2xl font-bold text-black">
28
+ {translate("verification.title")}
29
+ </Heading>
30
+ <Text className="mb-6 text-base text-gray-700">
31
+ {translate("verification.body", { appName: "{{APP_NAME}}" })}
32
+ </Text>
33
+ <CtaButton href={url}>{translate("verification.action")}</CtaButton>
34
+ </EmailLayout>
25
35
  );
26
36
  }
@@ -7,26 +7,26 @@ import VerificationEmail from "./emails/verificationEmail";
7
7
  export const templates = {
8
8
  accountDeletion: {
9
9
  id: "account-deletion",
10
- subject: "Delete your account",
11
- previewText: "Confirm your account deletion",
10
+ subject: "accountDeletion.subject",
11
+ previewText: "accountDeletion.previewText",
12
12
  react: AccountDeletionEmail as FunctionComponent<Record<string, unknown>>,
13
13
  },
14
14
  passwordReset: {
15
15
  id: "password-reset",
16
- subject: "Reset your password",
17
- previewText: "Reset your password request",
16
+ subject: "passwordReset.subject",
17
+ previewText: "passwordReset.previewText",
18
18
  react: PasswordResetEmail as FunctionComponent<Record<string, unknown>>,
19
19
  },
20
20
  verification: {
21
21
  id: "verification",
22
- subject: "Verify your email",
23
- previewText: "Verify your email address",
22
+ subject: "verification.subject",
23
+ previewText: "verification.previewText",
24
24
  react: VerificationEmail as FunctionComponent<Record<string, unknown>>,
25
25
  },
26
26
  organizationInvite: {
27
27
  id: "organization-invite",
28
- subject: "Join the organization",
29
- previewText: "You have been invited to join an organization",
28
+ subject: "organizationInvite.subject",
29
+ previewText: "organizationInvite.previewText",
30
30
  react: OrganizationInviteEmail as FunctionComponent<Record<string, unknown>>,
31
31
  },
32
32
  };
@@ -0,0 +1,36 @@
1
+ import type { BackendAppI18nResources } from "@m5kdev/backend/i18n/app-i18n";
2
+
3
+ export const emailResources = {
4
+ en: {
5
+ translation: {
6
+ verification: {
7
+ subject: "Verify your email",
8
+ previewText: "Verify your email address",
9
+ title: "Verify your email",
10
+ body: "Confirm your email address to finish setting up {{APP_NAME}} and access your editorial workspace.",
11
+ action: "Verify account",
12
+ },
13
+ passwordReset: {
14
+ subject: "Reset your password",
15
+ previewText: "Reset your password request",
16
+ title: "Reset your password",
17
+ body: "Use this link to choose a new password for your account.",
18
+ action: "Reset password",
19
+ },
20
+ accountDeletion: {
21
+ subject: "Delete your account",
22
+ previewText: "Confirm your account deletion",
23
+ title: "Confirm account deletion",
24
+ body: "Use this link to confirm deleting your account.",
25
+ action: "Confirm deletion",
26
+ },
27
+ organizationInvite: {
28
+ subject: "Join the organization",
29
+ previewText: "You have been invited to join an organization",
30
+ title: "Join {{organizationName}}",
31
+ body: "{{inviterName}} invited you to join {{organizationName}} as {{role}}.",
32
+ action: "Accept invitation",
33
+ },
34
+ },
35
+ },
36
+ } satisfies BackendAppI18nResources;
@@ -5,7 +5,10 @@ import { EmailModule } from "@m5kdev/backend/modules/email/email.module";
5
5
  import { NotificationModule } from "@m5kdev/backend/modules/notification/notification.module";
6
6
  import { WorkflowModule } from "@m5kdev/backend/modules/workflow/workflow.module";
7
7
  import { templates } from "{{PACKAGE_SCOPE}}/email";
8
+ import { emailResources } from "{{PACKAGE_SCOPE}}/email/resources";
8
9
  import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
10
+ import { AUTH_LOCALE_CONFIG } from "{{PACKAGE_SCOPE}}/shared/modules/app/locale.constants";
11
+ import { USER_LOCALE_HEADER } from "@m5kdev/commons/modules/auth/auth.constants";
9
12
  import cors from "cors";
10
13
  import express from "express";
11
14
  import { schema } from "./generated/schema";
@@ -46,6 +49,7 @@ app.use(
46
49
  "Waitlist-Invitation-Code",
47
50
  "Organization-Invitation-Code",
48
51
  "Admin-Create-Verified-User",
52
+ USER_LOCALE_HEADER,
49
53
  ],
50
54
  })
51
55
  );
@@ -61,6 +65,10 @@ export const builtBackendApp = createBackendApp(
61
65
  web: appUrl,
62
66
  api: serverUrl,
63
67
  },
68
+ locales: AUTH_LOCALE_CONFIG,
69
+ },
70
+ i18n: {
71
+ resources: emailResources,
64
72
  },
65
73
  redis: {
66
74
  url: redisUrl,
@@ -74,7 +82,7 @@ export const builtBackendApp = createBackendApp(
74
82
  outputDirectory: ".emails",
75
83
  },
76
84
  auth: {
77
- factory({ db, services, appConfig }) {
85
+ factory({ db, services, appConfig, i18n }) {
78
86
  return createBetterAuth({
79
87
  orm: db.orm as never,
80
88
  schema: db.schema as never,
@@ -82,6 +90,7 @@ export const builtBackendApp = createBackendApp(
82
90
  email: services.email.email,
83
91
  },
84
92
  app: appConfig,
93
+ i18n,
85
94
  config: {
86
95
  waitlist: enableWaitlist,
87
96
  provisionedAccountEmailDomain: "provisioned.{{APP_SLUG}}.local",
@@ -0,0 +1,7 @@
1
+ export const AUTH_LOCALE_CONFIG = {
2
+ defaultLocale: "en",
3
+ locales: [
4
+ { code: "en", displayName: "English" },
5
+ { code: "en_GB", displayName: "English (UK)" },
6
+ ],
7
+ } as const;
@@ -5,6 +5,7 @@ import { AuthOrganizationChildOrganizationsRoute } from "@m5kdev/web-ui/modules/
5
5
  import { AuthOrganizationMembersRoute } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationMembersRoute";
6
6
  import { AuthOrganizationPreferences } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationPreferences";
7
7
  import { AuthPublicRouter } from "@m5kdev/web-ui/modules/auth/components/AuthPublicRouter";
8
+ import { syncI18nLocale } from "@m5kdev/frontend/modules/app/utils/locale";
8
9
  import { AuthUserRouter } from "@m5kdev/web-ui/modules/auth/components/AuthUserRouter";
9
10
  import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
10
11
  import type { ReactNode } from "react";
@@ -65,6 +66,7 @@ export function Router() {
65
66
  {AuthPublicRouter({
66
67
  header: <AuthHeader />,
67
68
  waitlist: isWaitlist,
69
+ onLocaleChange: syncI18nLocale,
68
70
  })}
69
71
 
70
72
  <Route
@@ -97,6 +99,7 @@ export function Router() {
97
99
  {AuthUserRouter({
98
100
  schema: preferenceSchema,
99
101
  controls: preferenceControls,
102
+ onLocaleChange: syncI18nLocale,
100
103
  })}
101
104
  {AuthAdminRouter({
102
105
  enableWaitlist: true,
@@ -1,117 +0,0 @@
1
- import {
2
- Body,
3
- Button,
4
- Container,
5
- Head,
6
- Heading,
7
- Hr,
8
- Html,
9
- Preview,
10
- Section,
11
- Text,
12
- } from "@react-email/components";
13
- import type { ReactNode } from "react";
14
-
15
- interface BaseEmailProps {
16
- previewText: string;
17
- title: string;
18
- eyebrow: string;
19
- body: ReactNode;
20
- ctaLabel?: string;
21
- ctaUrl?: string;
22
- }
23
-
24
- export function BaseEmail({ previewText, title, eyebrow, body, ctaLabel, ctaUrl }: BaseEmailProps) {
25
- return (
26
- <Html>
27
- <Head />
28
- <Preview>{previewText}</Preview>
29
- <Body
30
- style={{
31
- backgroundColor: "#f3efe6",
32
- fontFamily: "'Manrope', 'Helvetica Neue', Arial, sans-serif",
33
- margin: "0",
34
- padding: "32px 0",
35
- }}
36
- >
37
- <Container
38
- style={{
39
- backgroundColor: "#fffaf1",
40
- border: "1px solid #d6c7ab",
41
- borderRadius: "24px",
42
- margin: "0 auto",
43
- maxWidth: "620px",
44
- padding: "40px",
45
- }}
46
- >
47
- <Text
48
- style={{
49
- color: "#8b5e34",
50
- fontSize: "12px",
51
- fontWeight: "700",
52
- letterSpacing: "0.18em",
53
- margin: "0 0 16px",
54
- textTransform: "uppercase",
55
- }}
56
- >
57
- {eyebrow}
58
- </Text>
59
- <Heading
60
- style={{
61
- color: "#171314",
62
- fontFamily: "'Cormorant Garamond', Georgia, serif",
63
- fontSize: "34px",
64
- lineHeight: "1.1",
65
- margin: "0 0 20px",
66
- }}
67
- >
68
- {title}
69
- </Heading>
70
- <Section
71
- style={{
72
- color: "#463630",
73
- fontSize: "16px",
74
- lineHeight: "1.7",
75
- }}
76
- >
77
- {body}
78
- </Section>
79
- {ctaLabel && ctaUrl ? (
80
- <Section style={{ marginTop: "28px" }}>
81
- <Button
82
- href={ctaUrl}
83
- style={{
84
- backgroundColor: "#1f4f46",
85
- borderRadius: "999px",
86
- color: "#fdf9f1",
87
- fontSize: "15px",
88
- fontWeight: "700",
89
- padding: "14px 24px",
90
- textDecoration: "none",
91
- }}
92
- >
93
- {ctaLabel}
94
- </Button>
95
- </Section>
96
- ) : null}
97
- <Hr
98
- style={{
99
- borderColor: "#e5dccd",
100
- margin: "28px 0",
101
- }}
102
- />
103
- <Text
104
- style={{
105
- color: "#7f6b5d",
106
- fontSize: "13px",
107
- lineHeight: "1.6",
108
- margin: "0",
109
- }}
110
- >
111
- Sent by {{APP_NAME}}.
112
- </Text>
113
- </Container>
114
- </Body>
115
- </Html>
116
- );
117
- }