create-m5kdev 0.21.2 → 0.21.4

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 (40) hide show
  1. package/package.json +1 -1
  2. package/templates/minimal-app/.cursor/rules/module-module-guide.mdc +18 -20
  3. package/templates/minimal-app/AGENTS.md.tpl +4 -3
  4. package/templates/minimal-app/apps/email/src/emails/accountDeletionEmail.tsx.tpl +8 -4
  5. package/templates/minimal-app/apps/server/AGENTS.md.tpl +6 -5
  6. package/templates/minimal-app/apps/server/drizzle/generate-schema.ts.tpl +1 -5
  7. package/templates/minimal-app/apps/server/drizzle/seed.ts.tpl +96 -27
  8. package/templates/minimal-app/apps/server/src/app.ts.tpl +84 -64
  9. package/templates/minimal-app/apps/server/src/generated/schema.ts.tpl +13 -0
  10. package/templates/minimal-app/apps/server/src/index.ts.tpl +4 -1
  11. package/templates/minimal-app/apps/server/src/lib/auth.ts.tpl +7 -1
  12. package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +1 -1
  13. package/templates/minimal-app/apps/server/src/modules/posts/posts.grants.ts.tpl +40 -40
  14. package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +5 -11
  15. package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +2 -2
  16. package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +4 -4
  17. package/templates/minimal-app/apps/server/src/modules/posts/posts.trpc.ts.tpl +1 -1
  18. package/templates/minimal-app/apps/server/src/types.ts.tpl +1 -3
  19. package/templates/minimal-app/apps/server/tsconfig.json.tpl +1 -0
  20. package/templates/minimal-app/apps/shared/.env.example.tpl +1 -0
  21. package/templates/minimal-app/apps/shared/.env.tpl +1 -0
  22. package/templates/minimal-app/apps/shared/src/modules/app/app.constants.ts.tpl +2 -0
  23. package/templates/minimal-app/apps/webapp/package.json.tpl +0 -1
  24. package/templates/minimal-app/apps/webapp/public/push-sw.js +2 -2
  25. package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +51 -40
  26. package/templates/minimal-app/apps/webapp/src/Providers.tsx.tpl +21 -11
  27. package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +76 -20
  28. package/templates/minimal-app/apps/webapp/src/modules/notification/PushNotificationsPanel.tsx.tpl +12 -16
  29. package/templates/minimal-app/apps/webapp/src/modules/posts/PostsRoute.tsx.tpl +121 -119
  30. package/templates/minimal-app/apps/webapp/src/utils/trpc.ts.tpl +5 -3
  31. package/templates/minimal-app/apps/webapp/translations/en/blog-app.json.tpl +13 -1
  32. package/templates/minimal-app/apps/webapp/tsconfig.json.tpl +1 -0
  33. package/templates/minimal-app/pnpm-workspace.yaml.tpl +8 -8
  34. package/templates/minimal-app/apps/server/src/modules.ts.tpl +0 -33
  35. package/templates/minimal-app/apps/server/src/repository.ts.tpl +0 -6
  36. package/templates/minimal-app/apps/server/src/schema-modules.ts.tpl +0 -19
  37. package/templates/minimal-app/apps/server/src/service.ts.tpl +0 -7
  38. package/templates/minimal-app/apps/server/src/trpc.ts.tpl +0 -6
  39. package/templates/minimal-app/apps/server/src/workflow.ts.tpl +0 -4
  40. 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
- PostsModuleDeps,
26
+ never,
29
27
  PostsModuleTables,
30
28
  PostsModuleRepositories,
31
29
  PostsModuleServices,
32
30
  PostsModuleRouters
33
31
  > {
34
32
  readonly id = "posts";
35
- override readonly dependsOn = ["auth"] as const;
33
+ override readonly dbDependsOn = ["auth"] as const;
36
34
 
37
- override repositories({ db }: ModuleRepositoriesContext<PostsModuleDeps, PostsModuleTables>) {
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<PostsModuleDeps, PostsModuleServices>) {
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.publishedAt ?? new Date(),
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(
@@ -1,3 +1 @@
1
- import type { AppRouter, RouterInputs, RouterOutputs } from "./trpc";
2
-
3
- export type { AppRouter, RouterInputs, RouterOutputs };
1
+ export type { AppRouter } from "./app";
@@ -3,6 +3,7 @@
3
3
  "rootDir": ".",
4
4
  "compilerOptions": {
5
5
  "esModuleInterop": true,
6
+ "ignoreDeprecations": "6.0",
6
7
  "importHelpers": true,
7
8
  "jsx": "react-jsx",
8
9
  "noEmit": true,
@@ -5,6 +5,7 @@ BETTER_AUTH_SECRET=replace-me
5
5
  VITE_APP_NAME={{APP_NAME}}
6
6
  VITE_APP_URL=http://localhost:5173
7
7
  VITE_SERVER_URL=http://localhost:8080
8
+ VITE_ENABLE_WAITLIST=false
8
9
 
9
10
  # Database
10
11
  DATABASE_URL=file:./local.db
@@ -5,6 +5,7 @@ BETTER_AUTH_SECRET={{BETTER_AUTH_SECRET}}
5
5
  VITE_APP_NAME={{APP_NAME}}
6
6
  VITE_APP_URL=http://localhost:5173
7
7
  VITE_SERVER_URL=http://localhost:8080
8
+ VITE_ENABLE_WAITLIST=false
8
9
 
9
10
  # Database
10
11
  DATABASE_URL=file:./local.db
@@ -0,0 +1,2 @@
1
+ export const APP_NAME = "{{APP_NAME}}";
2
+ export const APP_SLUG = "{{APP_SLUG}}";
@@ -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 { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
2
- import { Button, Chip } from "@heroui/react";
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-app");
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 color="warning" variant="flat">
54
- {t("layout.workspace.local")}
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
- <NavLink
66
- to="/posts"
67
- className={({ isActive }) =>
68
- isActive
69
- ? "group flex items-center gap-3 rounded-[22px] border border-emerald-300 bg-emerald-950 px-4 py-3 text-emerald-50"
70
- : "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"
71
- }
72
- >
73
- <BookTextIcon className="h-4 w-4" />
74
- <span className="font-medium">{t("layout.navigation.posts")}</span>
75
- </NavLink>
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
- <Button
86
- radius="full"
87
- size="sm"
88
- variant="flat"
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
- </Button>
99
- <Button
100
- radius="full"
101
- size="sm"
102
- color="danger"
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
- </Button>
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
- <ThemeProvider defaultTheme="light" storageKey="m5kdev-theme">
12
- <AuthProvider loader={<AppLoader />}>
13
- <TrpcQueryProvider>
14
- <DialogProvider>
15
- <Router />
16
- </DialogProvider>
17
- <Toaster richColors closeButton />
18
- </TrpcQueryProvider>
19
- </AuthProvider>
20
- </ThemeProvider>
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 { AuthRouter } from "@m5kdev/web-ui/modules/auth/components/AuthRouter";
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 { Navigate, Route, Routes } from "react-router";
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
- function ProtectedRoutes({ children }: { children: ReactNode }) {
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 <>{children}</>;
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.32em] text-amber-700/80">
23
- Editorial Workspace
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-ink/70">
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
- {AuthRouter({
65
+ {AuthPublicRouter({
37
66
  header: <AuthHeader />,
67
+ waitlist: isWaitlist,
38
68
  })}
39
69
 
40
70
  <Route
41
- path="/"
42
- element={
43
- <ProtectedRoutes>
44
- <Layout />
45
- </ProtectedRoutes>
46
- }
47
- >
48
- <Route index element={<Navigate to="/posts" replace />} />
49
- <Route path="posts" element={<PostsRoute />} />
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
  );
@@ -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 "../../utils/trpc";
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-app");
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: trpc.notification.vapidPublicKey.queryOptions(),
40
- registerDeviceMutation: trpc.notification.registerDevice.mutationOptions(),
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 className="mt-3" color={flowStatus === "error" ? "danger" : "success"} variant="flat">
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>