create-m5kdev 0.21.3 → 0.21.5
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 +1 -1
- package/templates/minimal-app/.cursor/rules/module-module-guide.mdc +18 -20
- package/templates/minimal-app/AGENTS.md.tpl +4 -3
- package/templates/minimal-app/apps/email/src/emails/accountDeletionEmail.tsx.tpl +8 -4
- package/templates/minimal-app/apps/server/AGENTS.md.tpl +6 -5
- package/templates/minimal-app/apps/server/drizzle/generate-schema.ts.tpl +1 -5
- package/templates/minimal-app/apps/server/drizzle/seed.ts.tpl +96 -27
- package/templates/minimal-app/apps/server/src/app.ts.tpl +84 -64
- package/templates/minimal-app/apps/server/src/generated/schema.ts.tpl +13 -0
- package/templates/minimal-app/apps/server/src/index.ts.tpl +4 -1
- package/templates/minimal-app/apps/server/src/lib/auth.ts.tpl +7 -1
- package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +1 -1
- package/templates/minimal-app/apps/server/src/modules/posts/posts.grants.ts.tpl +40 -40
- package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +5 -11
- package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +2 -2
- package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +4 -4
- package/templates/minimal-app/apps/server/src/modules/posts/posts.trpc.ts.tpl +1 -1
- package/templates/minimal-app/apps/server/src/types.ts.tpl +1 -3
- package/templates/minimal-app/apps/server/tsconfig.json.tpl +1 -0
- package/templates/minimal-app/apps/shared/.env.example.tpl +1 -0
- package/templates/minimal-app/apps/shared/.env.tpl +1 -0
- package/templates/minimal-app/apps/shared/src/modules/app/app.constants.ts.tpl +2 -0
- package/templates/minimal-app/apps/webapp/package.json.tpl +0 -1
- package/templates/minimal-app/apps/webapp/public/push-sw.js +2 -2
- package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +51 -40
- package/templates/minimal-app/apps/webapp/src/Providers.tsx.tpl +21 -11
- package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +76 -20
- package/templates/minimal-app/apps/webapp/src/modules/notification/PushNotificationsPanel.tsx.tpl +12 -16
- package/templates/minimal-app/apps/webapp/src/modules/posts/PostsRoute.tsx.tpl +121 -119
- package/templates/minimal-app/apps/webapp/src/utils/trpc.ts.tpl +5 -3
- package/templates/minimal-app/apps/webapp/translations/en/blog-app.json.tpl +13 -1
- package/templates/minimal-app/apps/webapp/tsconfig.json.tpl +1 -0
- package/templates/minimal-app/pnpm-workspace.yaml.tpl +8 -8
- package/templates/minimal-app/apps/server/src/modules.ts.tpl +0 -33
- package/templates/minimal-app/apps/server/src/repository.ts.tpl +0 -6
- package/templates/minimal-app/apps/server/src/schema-modules.ts.tpl +0 -19
- package/templates/minimal-app/apps/server/src/service.ts.tpl +0 -7
- package/templates/minimal-app/apps/server/src/trpc.ts.tpl +0 -6
- package/templates/minimal-app/apps/server/src/workflow.ts.tpl +0 -4
- package/templates/minimal-app/apps/webapp/src/components/TrpcQueryProvider.tsx.tpl +0 -61
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createBackendRouterMap } from "@m5kdev/backend/app";
|
|
2
|
-
import type { AuthModule } from "@m5kdev/backend/modules/auth/auth.module";
|
|
3
2
|
import {
|
|
4
3
|
BaseModule,
|
|
5
4
|
type ModuleRepositoriesContext,
|
|
@@ -12,7 +11,6 @@ import { PostsRepository } from "./posts.repository";
|
|
|
12
11
|
import { PostsService } from "./posts.service";
|
|
13
12
|
import { createPostsTRPC } from "./posts.trpc";
|
|
14
13
|
|
|
15
|
-
type PostsModuleDeps = { auth: AuthModule };
|
|
16
14
|
type PostsModuleTables = typeof postsTables;
|
|
17
15
|
type PostsModuleRepositories = {
|
|
18
16
|
posts: PostsRepository;
|
|
@@ -25,16 +23,16 @@ type PostsModuleRouters = {
|
|
|
25
23
|
};
|
|
26
24
|
|
|
27
25
|
export class PostsModule extends BaseModule<
|
|
28
|
-
|
|
26
|
+
never,
|
|
29
27
|
PostsModuleTables,
|
|
30
28
|
PostsModuleRepositories,
|
|
31
29
|
PostsModuleServices,
|
|
32
30
|
PostsModuleRouters
|
|
33
31
|
> {
|
|
34
32
|
readonly id = "posts";
|
|
35
|
-
override readonly
|
|
33
|
+
override readonly dbDependsOn = ["auth"] as const;
|
|
36
34
|
|
|
37
|
-
override repositories({ db }: ModuleRepositoriesContext<
|
|
35
|
+
override repositories({ db }: ModuleRepositoriesContext<never, PostsModuleTables>) {
|
|
38
36
|
return {
|
|
39
37
|
posts: new PostsRepository({
|
|
40
38
|
orm: db.orm,
|
|
@@ -44,17 +42,13 @@ export class PostsModule extends BaseModule<
|
|
|
44
42
|
};
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
override services({
|
|
48
|
-
repositories,
|
|
49
|
-
}: ModuleServicesContext<PostsModuleDeps, PostsModuleRepositories>) {
|
|
45
|
+
override services({ repositories }: ModuleServicesContext<never, PostsModuleRepositories>) {
|
|
50
46
|
return {
|
|
51
47
|
posts: new PostsService({ posts: repositories.posts }, {}, postsGrants),
|
|
52
48
|
};
|
|
53
49
|
}
|
|
54
50
|
|
|
55
|
-
override trpc({ trpc, services }: ModuleTRPCContext<
|
|
51
|
+
override trpc({ trpc, services }: ModuleTRPCContext<never, PostsModuleServices>) {
|
|
56
52
|
return createBackendRouterMap("posts", createPostsTRPC(trpc, services.posts));
|
|
57
53
|
}
|
|
58
54
|
}
|
|
59
|
-
|
|
60
|
-
export const postsModule = new PostsModule();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { BaseTableRepository } from "@m5kdev/backend/modules/base/base.repository";
|
|
2
|
+
import type { ServerResultAsync } from "@m5kdev/backend/utils/types";
|
|
1
3
|
import type {
|
|
2
4
|
PostSchema,
|
|
3
5
|
PostsListInputSchema,
|
|
4
6
|
PostsListOutputSchema,
|
|
5
7
|
} 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
8
|
import { and, asc, count, desc, eq, isNull, like, ne, or } from "drizzle-orm";
|
|
9
9
|
import type { LibSQLDatabase } from "drizzle-orm/libsql";
|
|
10
10
|
import { err, ok } from "neverthrow";
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { BasePermissionService } from "@m5kdev/backend/modules/base/base.service";
|
|
2
|
+
import type { Context } from "@m5kdev/backend/utils/trpc";
|
|
3
|
+
import type { ServerResultAsync } from "@m5kdev/backend/utils/types";
|
|
1
4
|
import type {
|
|
2
5
|
PostCreateInputSchema,
|
|
3
6
|
PostCreateOutputSchema,
|
|
@@ -10,9 +13,6 @@ import type {
|
|
|
10
13
|
PostUpdateInputSchema,
|
|
11
14
|
PostUpdateOutputSchema,
|
|
12
15
|
} from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.schema";
|
|
13
|
-
import { BasePermissionService } from "@m5kdev/backend/modules/base/base.service";
|
|
14
|
-
import type { ServerResultAsync } from "@m5kdev/backend/utils/types";
|
|
15
|
-
import type { Context } from "@m5kdev/backend/utils/trpc";
|
|
16
16
|
import { err, ok } from "neverthrow";
|
|
17
17
|
import type { PostsRepository } from "./posts.repository";
|
|
18
18
|
|
|
@@ -114,7 +114,7 @@ export class PostsService extends BasePermissionService<
|
|
|
114
114
|
return this.repository.posts.update({
|
|
115
115
|
id: input.id,
|
|
116
116
|
status: "published",
|
|
117
|
-
publishedAt: state.post
|
|
117
|
+
publishedAt: state.post?.publishedAt ?? new Date(),
|
|
118
118
|
}) as ServerResultAsync<PostPublishOutputSchema>;
|
|
119
119
|
});
|
|
120
120
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { handleTRPCResult, type TRPCMethods } from "@m5kdev/backend/utils/trpc";
|
|
1
2
|
import {
|
|
2
3
|
postCreateInputSchema,
|
|
3
4
|
postCreateOutputSchema,
|
|
@@ -10,7 +11,6 @@ import {
|
|
|
10
11
|
postUpdateInputSchema,
|
|
11
12
|
postUpdateOutputSchema,
|
|
12
13
|
} from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.schema";
|
|
13
|
-
import { handleTRPCResult, type TRPCMethods } from "@m5kdev/backend/utils/trpc";
|
|
14
14
|
import type { PostsService } from "./posts.service";
|
|
15
15
|
|
|
16
16
|
export function createPostsTRPC(
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"@m5kdev/frontend": "catalog:",
|
|
20
20
|
"@m5kdev/web-ui": "catalog:",
|
|
21
21
|
"@tanstack/react-query": "catalog:",
|
|
22
|
-
"@tanstack/react-query-devtools": "catalog:",
|
|
23
22
|
"@trpc/client": "catalog:",
|
|
24
23
|
"@trpc/tanstack-react-query": "catalog:",
|
|
25
24
|
"better-auth": "catalog:",
|
|
@@ -13,7 +13,7 @@ self.addEventListener("push", (event) => {
|
|
|
13
13
|
self.registration.showNotification(payload.title, {
|
|
14
14
|
body: payload.body,
|
|
15
15
|
data: payload.data ?? {},
|
|
16
|
-
})
|
|
16
|
+
})
|
|
17
17
|
);
|
|
18
18
|
});
|
|
19
19
|
|
|
@@ -29,6 +29,6 @@ self.addEventListener("notificationclick", (event) => {
|
|
|
29
29
|
return self.clients.openWindow("/");
|
|
30
30
|
}
|
|
31
31
|
return undefined;
|
|
32
|
-
})
|
|
32
|
+
})
|
|
33
33
|
);
|
|
34
34
|
});
|
|
@@ -1,23 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Chip } from "@heroui/react";
|
|
2
|
+
import { buttonVariants } from "@heroui/styles";
|
|
3
3
|
import { authClient } from "@m5kdev/frontend/modules/auth/auth.lib";
|
|
4
4
|
import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
|
|
5
5
|
import { useTheme } from "@m5kdev/web-ui/components/theme-provider";
|
|
6
|
+
import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
|
|
6
7
|
import {
|
|
7
8
|
BookTextIcon,
|
|
9
|
+
Building2Icon,
|
|
8
10
|
LogOutIcon,
|
|
9
11
|
MoonStarIcon,
|
|
10
12
|
NotebookTextIcon,
|
|
13
|
+
SettingsIcon,
|
|
14
|
+
ShieldCheckIcon,
|
|
11
15
|
SunMediumIcon,
|
|
16
|
+
UserCogIcon,
|
|
17
|
+
UsersIcon,
|
|
12
18
|
} from "lucide-react";
|
|
13
19
|
import { useTranslation } from "react-i18next";
|
|
14
20
|
import { NavLink, Outlet } from "react-router";
|
|
15
21
|
import { PushNotificationsPanel } from "./modules/notification/PushNotificationsPanel";
|
|
16
22
|
|
|
23
|
+
const NAV_LINKS = [
|
|
24
|
+
{ to: "/posts", labelKey: "layout.navigation.posts", icon: BookTextIcon },
|
|
25
|
+
{ to: "/organization/members", labelKey: "layout.navigation.members", icon: UsersIcon },
|
|
26
|
+
{ to: "/organization/manage", labelKey: "layout.navigation.childOrgs", icon: Building2Icon },
|
|
27
|
+
{
|
|
28
|
+
to: "/organization/preferences",
|
|
29
|
+
labelKey: "layout.navigation.orgPreferences",
|
|
30
|
+
icon: SettingsIcon,
|
|
31
|
+
},
|
|
32
|
+
{ to: "/user/preferences", labelKey: "layout.navigation.profile", icon: UserCogIcon },
|
|
33
|
+
{ to: "/user/invite", labelKey: "layout.navigation.invites", icon: UsersIcon },
|
|
34
|
+
{ to: "/admin/users", labelKey: "layout.navigation.admin", icon: ShieldCheckIcon },
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
17
37
|
export function Layout() {
|
|
18
38
|
const { data: session } = useSession();
|
|
19
39
|
const { theme, setTheme } = useTheme();
|
|
20
|
-
const { t } = useTranslation("blog
|
|
40
|
+
const { t } = useTranslation("blog{{PACKAGE_SCOPE}}");
|
|
21
41
|
|
|
22
42
|
const userName = session?.user?.name || session?.user?.email || "Editor";
|
|
23
43
|
const userEmail = session?.user?.email || "";
|
|
@@ -50,29 +70,28 @@ export function Layout() {
|
|
|
50
70
|
</p>
|
|
51
71
|
<p className="mt-3 text-sm leading-6 text-ink/75">{t("layout.workspace.body")}</p>
|
|
52
72
|
<div className="mt-4 flex flex-wrap gap-2">
|
|
53
|
-
<Chip
|
|
54
|
-
|
|
55
|
-
</Chip>
|
|
56
|
-
<Chip color="success" variant="flat">
|
|
57
|
-
{t("layout.workspace.auth")}
|
|
58
|
-
</Chip>
|
|
73
|
+
<Chip className="bg-amber-100 text-amber-900">{t("layout.workspace.local")}</Chip>
|
|
74
|
+
<Chip className="bg-emerald-100 text-emerald-900">{t("layout.workspace.auth")}</Chip>
|
|
59
75
|
</div>
|
|
60
76
|
</div>
|
|
61
77
|
|
|
62
78
|
<PushNotificationsPanel />
|
|
63
79
|
|
|
64
80
|
<nav className="mt-8 grid gap-2">
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
{NAV_LINKS.map(({ to, labelKey, icon: Icon }) => (
|
|
82
|
+
<NavLink
|
|
83
|
+
key={to}
|
|
84
|
+
to={to}
|
|
85
|
+
className={({ isActive }) =>
|
|
86
|
+
isActive
|
|
87
|
+
? "group flex items-center gap-3 rounded-[22px] border border-emerald-300 bg-emerald-950 px-4 py-3 text-emerald-50"
|
|
88
|
+
: "group flex items-center gap-3 rounded-[22px] border border-transparent bg-transparent px-4 py-3 text-ink/80 transition hover:border-amber-200 hover:bg-white/70"
|
|
89
|
+
}
|
|
90
|
+
>
|
|
91
|
+
<Icon className="h-4 w-4" />
|
|
92
|
+
<span className="font-medium">{t(labelKey)}</span>
|
|
93
|
+
</NavLink>
|
|
94
|
+
))}
|
|
76
95
|
</nav>
|
|
77
96
|
|
|
78
97
|
<div className="mt-8 rounded-[28px] border border-white/60 bg-stone-950 px-4 py-4 text-stone-100 shadow-[0_16px_32px_rgba(23,19,20,0.2)]">
|
|
@@ -81,13 +100,11 @@ export function Layout() {
|
|
|
81
100
|
</p>
|
|
82
101
|
<p className="mt-3 text-lg font-semibold">{userName}</p>
|
|
83
102
|
<p className="mt-1 text-sm text-stone-300">{userEmail}</p>
|
|
84
|
-
<div className="mt-4 flex items-center gap-2">
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
className="bg-stone-800 text-stone-100"
|
|
90
|
-
onPress={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
103
|
+
<div className="mt-4 flex flex-wrap items-center gap-2">
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
className={buttonVariants({ variant: "secondary", size: "sm" })}
|
|
107
|
+
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
91
108
|
>
|
|
92
109
|
{theme === "dark" ? (
|
|
93
110
|
<SunMediumIcon className="h-4 w-4" />
|
|
@@ -95,20 +112,18 @@ export function Layout() {
|
|
|
95
112
|
<MoonStarIcon className="h-4 w-4" />
|
|
96
113
|
)}
|
|
97
114
|
{theme === "dark" ? t("layout.account.light") : t("layout.account.dark")}
|
|
98
|
-
</
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
variant="flat"
|
|
104
|
-
onPress={() => {
|
|
115
|
+
</button>
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
className={buttonVariants({ variant: "danger", size: "sm" })}
|
|
119
|
+
onClick={() => {
|
|
105
120
|
authClient.signOut();
|
|
106
121
|
window.location.assign("/login");
|
|
107
122
|
}}
|
|
108
123
|
>
|
|
109
124
|
<LogOutIcon className="h-4 w-4" />
|
|
110
125
|
{t("layout.account.signOut")}
|
|
111
|
-
</
|
|
126
|
+
</button>
|
|
112
127
|
</div>
|
|
113
128
|
</div>
|
|
114
129
|
</aside>
|
|
@@ -123,11 +138,7 @@ export function Layout() {
|
|
|
123
138
|
{t("layout.header.title")}
|
|
124
139
|
</h2>
|
|
125
140
|
</div>
|
|
126
|
-
<Chip
|
|
127
|
-
radius="full"
|
|
128
|
-
className="border border-emerald-300 bg-emerald-50 px-4 py-5 text-emerald-900"
|
|
129
|
-
variant="bordered"
|
|
130
|
-
>
|
|
141
|
+
<Chip className="border border-emerald-300 bg-emerald-50 px-4 py-5 text-emerald-900">
|
|
131
142
|
{t("layout.header.runtime")}
|
|
132
143
|
</Chip>
|
|
133
144
|
</header>
|
|
@@ -1,22 +1,32 @@
|
|
|
1
|
+
import { AppConfigProvider } from "@m5kdev/frontend/modules/app/components/AppConfigProvider";
|
|
2
|
+
import { AppTrpcQueryProvider } from "@m5kdev/frontend/modules/app/components/AppTrpcQueryProvider";
|
|
1
3
|
import { AuthProvider } from "@m5kdev/frontend/modules/auth/components/AuthProvider";
|
|
2
4
|
import { DialogProvider } from "@m5kdev/web-ui/components/DialogProvider";
|
|
3
5
|
import { ThemeProvider } from "@m5kdev/web-ui/components/theme-provider";
|
|
4
6
|
import { AppLoader } from "@m5kdev/web-ui/modules/app/components/AppLoader";
|
|
7
|
+
import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
|
|
5
8
|
import { Toaster } from "sonner";
|
|
6
|
-
import { TrpcQueryProvider } from "@/components/TrpcQueryProvider";
|
|
7
9
|
import { Router } from "./Router";
|
|
8
10
|
|
|
9
11
|
export function Providers() {
|
|
10
12
|
return (
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
<AppConfigProvider
|
|
14
|
+
config={{
|
|
15
|
+
appName: APP_NAME,
|
|
16
|
+
appUrl: import.meta.env.VITE_APP_URL,
|
|
17
|
+
serverUrl: import.meta.env.VITE_SERVER_URL,
|
|
18
|
+
}}
|
|
19
|
+
>
|
|
20
|
+
<ThemeProvider defaultTheme="light" storageKey="m5kdev-theme">
|
|
21
|
+
<AuthProvider loader={<AppLoader />}>
|
|
22
|
+
<AppTrpcQueryProvider>
|
|
23
|
+
<DialogProvider>
|
|
24
|
+
<Router />
|
|
25
|
+
</DialogProvider>
|
|
26
|
+
<Toaster richColors closeButton />
|
|
27
|
+
</AppTrpcQueryProvider>
|
|
28
|
+
</AuthProvider>
|
|
29
|
+
</ThemeProvider>
|
|
30
|
+
</AppConfigProvider>
|
|
21
31
|
);
|
|
22
32
|
}
|
|
@@ -1,52 +1,108 @@
|
|
|
1
|
-
import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
|
|
2
1
|
import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
|
|
3
|
-
import {
|
|
2
|
+
import { AuthAdminRouter } from "@m5kdev/web-ui/modules/auth/components/AuthAdminRouter";
|
|
3
|
+
import { AuthOrganizationAcceptInvitationRoute } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationAcceptInvitationRoute";
|
|
4
|
+
import { AuthOrganizationChildOrganizationsRoute } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationChildOrganizationsRoute";
|
|
5
|
+
import { AuthOrganizationMembersRoute } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationMembersRoute";
|
|
6
|
+
import { AuthOrganizationPreferences } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationPreferences";
|
|
7
|
+
import { AuthPublicRouter } from "@m5kdev/web-ui/modules/auth/components/AuthPublicRouter";
|
|
8
|
+
import { AuthUserRouter } from "@m5kdev/web-ui/modules/auth/components/AuthUserRouter";
|
|
9
|
+
import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
|
|
4
10
|
import type { ReactNode } from "react";
|
|
5
|
-
import {
|
|
11
|
+
import { useTranslation } from "react-i18next";
|
|
12
|
+
import { Navigate, Outlet, Route, Routes } from "react-router";
|
|
13
|
+
import { z } from "zod";
|
|
6
14
|
import { PostsRoute } from "@/modules/posts/PostsRoute";
|
|
7
15
|
import { Layout } from "./Layout";
|
|
8
16
|
|
|
9
|
-
|
|
17
|
+
const preferenceSchema = z.object({
|
|
18
|
+
compactMode: z.boolean().optional(),
|
|
19
|
+
postsPerPage: z.number().min(1).max(20).optional(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const preferenceControls = {
|
|
23
|
+
compactMode: {
|
|
24
|
+
label: "Compact mode",
|
|
25
|
+
element: "switch",
|
|
26
|
+
},
|
|
27
|
+
postsPerPage: {
|
|
28
|
+
label: "Posts per page",
|
|
29
|
+
element: "number",
|
|
30
|
+
min: 1,
|
|
31
|
+
max: 20,
|
|
32
|
+
step: 1,
|
|
33
|
+
},
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
function ProtectedRoutes({ children }: { children?: ReactNode }) {
|
|
10
37
|
const { data: session } = useSession();
|
|
11
38
|
|
|
12
39
|
if (!session) {
|
|
13
40
|
return <Navigate to="/login" replace />;
|
|
14
41
|
}
|
|
15
42
|
|
|
16
|
-
return
|
|
43
|
+
return children ?? <Outlet />;
|
|
17
44
|
}
|
|
18
45
|
|
|
19
46
|
function AuthHeader() {
|
|
47
|
+
const { t } = useTranslation("blog{{PACKAGE_SCOPE}}");
|
|
48
|
+
|
|
20
49
|
return (
|
|
21
50
|
<div className="text-center">
|
|
22
|
-
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.
|
|
23
|
-
|
|
51
|
+
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.3em] text-default-500">
|
|
52
|
+
{t("auth.header.eyebrow")}
|
|
24
53
|
</p>
|
|
25
54
|
<h1 className="mt-3 font-editorial text-4xl text-ink">{APP_NAME}</h1>
|
|
26
|
-
<p className="mt-3 text-sm text-
|
|
27
|
-
A minimal m5kdev starter with auth, tRPC, and one polished posts module.
|
|
28
|
-
</p>
|
|
55
|
+
<p className="mt-3 text-sm text-default-600">{t("auth.header.tagline")}</p>
|
|
29
56
|
</div>
|
|
30
57
|
);
|
|
31
58
|
}
|
|
32
59
|
|
|
33
60
|
export function Router() {
|
|
61
|
+
const isWaitlist = import.meta.env.VITE_ENABLE_WAITLIST === "true";
|
|
62
|
+
|
|
34
63
|
return (
|
|
35
64
|
<Routes>
|
|
36
|
-
{
|
|
65
|
+
{AuthPublicRouter({
|
|
37
66
|
header: <AuthHeader />,
|
|
67
|
+
waitlist: isWaitlist,
|
|
38
68
|
})}
|
|
39
69
|
|
|
40
70
|
<Route
|
|
41
|
-
path="/"
|
|
42
|
-
element={
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
71
|
+
path="/organization/accept-invitation"
|
|
72
|
+
element={<AuthOrganizationAcceptInvitationRoute />}
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
<Route element={<ProtectedRoutes />}>
|
|
76
|
+
<Route path="/" element={<Layout />}>
|
|
77
|
+
<Route index element={<Navigate to="/posts" replace />} />
|
|
78
|
+
<Route path="posts" element={<PostsRoute />} />
|
|
79
|
+
<Route
|
|
80
|
+
path="organization/members"
|
|
81
|
+
element={<AuthOrganizationMembersRoute managerRoles={["admin", "owner"]} />}
|
|
82
|
+
/>
|
|
83
|
+
<Route
|
|
84
|
+
path="organization/manage"
|
|
85
|
+
element={<AuthOrganizationChildOrganizationsRoute managerRoles={["admin", "owner"]} />}
|
|
86
|
+
/>
|
|
87
|
+
<Route
|
|
88
|
+
path="organization/preferences"
|
|
89
|
+
element={
|
|
90
|
+
<AuthOrganizationPreferences
|
|
91
|
+
schema={preferenceSchema}
|
|
92
|
+
controls={preferenceControls}
|
|
93
|
+
managerRoles={["admin", "owner"]}
|
|
94
|
+
/>
|
|
95
|
+
}
|
|
96
|
+
/>
|
|
97
|
+
{AuthUserRouter({
|
|
98
|
+
schema: preferenceSchema,
|
|
99
|
+
controls: preferenceControls,
|
|
100
|
+
})}
|
|
101
|
+
{AuthAdminRouter({
|
|
102
|
+
enableWaitlist: true,
|
|
103
|
+
enableAccountClaimActions: true,
|
|
104
|
+
})}
|
|
105
|
+
</Route>
|
|
50
106
|
</Route>
|
|
51
107
|
</Routes>
|
|
52
108
|
);
|
package/templates/minimal-app/apps/webapp/src/modules/notification/PushNotificationsPanel.tsx.tpl
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { Button, Chip } from "@heroui/react";
|
|
2
2
|
import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
|
|
3
|
-
import { useWebPush } from "@m5kdev/web-ui/hooks/useWebPush";
|
|
3
|
+
import { type UseWebPushOptions, useWebPush } from "@m5kdev/web-ui/hooks/useWebPush";
|
|
4
4
|
import { BellIcon } from "lucide-react";
|
|
5
5
|
import { useMemo } from "react";
|
|
6
6
|
import { useTranslation } from "react-i18next";
|
|
7
|
-
import { useTRPC } from "
|
|
7
|
+
import { useTRPC } from "@/utils/trpc";
|
|
8
8
|
|
|
9
9
|
export function PushNotificationsPanel() {
|
|
10
10
|
const { data: session } = useSession();
|
|
11
11
|
const trpc = useTRPC();
|
|
12
|
-
const { t } = useTranslation("blog
|
|
12
|
+
const { t } = useTranslation("blog{{PACKAGE_SCOPE}}");
|
|
13
13
|
|
|
14
14
|
const messages = useMemo(
|
|
15
15
|
() => ({
|
|
@@ -20,7 +20,7 @@ export function PushNotificationsPanel() {
|
|
|
20
20
|
failed: t("layout.push.failed"),
|
|
21
21
|
enabled: t("layout.push.enabled"),
|
|
22
22
|
}),
|
|
23
|
-
[t]
|
|
23
|
+
[t]
|
|
24
24
|
);
|
|
25
25
|
|
|
26
26
|
const {
|
|
@@ -36,8 +36,10 @@ export function PushNotificationsPanel() {
|
|
|
36
36
|
} = useWebPush({
|
|
37
37
|
enabled: Boolean(session),
|
|
38
38
|
messages,
|
|
39
|
-
vapidPublicKeyQuery:
|
|
40
|
-
|
|
39
|
+
vapidPublicKeyQuery:
|
|
40
|
+
trpc.notification.vapidPublicKey.queryOptions() as unknown as UseWebPushOptions["vapidPublicKeyQuery"],
|
|
41
|
+
registerDeviceMutation:
|
|
42
|
+
trpc.notification.registerDevice.mutationOptions() as unknown as UseWebPushOptions["registerDeviceMutation"],
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
if (!session) {
|
|
@@ -71,19 +73,13 @@ export function PushNotificationsPanel() {
|
|
|
71
73
|
<p className="mt-2 text-xs text-emerald-800/90">{t("layout.push.permissionGranted")}</p>
|
|
72
74
|
) : null}
|
|
73
75
|
{feedback ? (
|
|
74
|
-
<Chip
|
|
76
|
+
<Chip
|
|
77
|
+
className={`mt-3 ${flowStatus === "error" ? "bg-danger-100 text-danger-900" : "bg-success-100 text-success-900"}`}
|
|
78
|
+
>
|
|
75
79
|
{feedback}
|
|
76
80
|
</Chip>
|
|
77
81
|
) : null}
|
|
78
|
-
<Button
|
|
79
|
-
className="mt-3"
|
|
80
|
-
color="primary"
|
|
81
|
-
isDisabled={disabled}
|
|
82
|
-
radius="full"
|
|
83
|
-
size="sm"
|
|
84
|
-
variant="flat"
|
|
85
|
-
onPress={() => void subscribe()}
|
|
86
|
-
>
|
|
82
|
+
<Button className="mt-3" isDisabled={disabled} size="sm" onPress={() => void subscribe()}>
|
|
87
83
|
{t("layout.push.cta")}
|
|
88
84
|
</Button>
|
|
89
85
|
</div>
|