create-m5kdev 0.25.0 → 0.25.2

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 (29) hide show
  1. package/package.json +1 -1
  2. package/templates/minimal-app/.cursor/rules/frontend-component-guide.mdc +95 -0
  3. package/templates/minimal-app/.cursor/rules/frontend-data-guide.mdc +59 -0
  4. package/templates/minimal-app/.cursor/rules/frontend-form-guide.mdc +80 -0
  5. package/templates/minimal-app/.cursor/rules/frontend-hook-guide.mdc +79 -0
  6. package/templates/minimal-app/.cursor/rules/frontend-i18n-guide.mdc +45 -0
  7. package/templates/minimal-app/.cursor/rules/module-db-guide.mdc +1 -1
  8. package/templates/minimal-app/.cursor/rules/module-grants-guide.mdc +1 -1
  9. package/templates/minimal-app/.cursor/rules/module-module-guide.mdc +1 -1
  10. package/templates/minimal-app/.cursor/rules/module-repository-guide.mdc +1 -1
  11. package/templates/minimal-app/.cursor/rules/module-schema-guide.mdc +1 -1
  12. package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +1 -1
  13. package/templates/minimal-app/.cursor/rules/module-trpc-guide.mdc +1 -1
  14. package/templates/minimal-app/AGENTS.md.tpl +7 -0
  15. package/templates/minimal-app/apps/email/package.json.tpl +3 -3
  16. package/templates/minimal-app/apps/webapp/AGENTS.md.tpl +24 -18
  17. package/templates/minimal-app/apps/webapp/public/logo.svg +6 -0
  18. package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +59 -153
  19. package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +100 -111
  20. package/templates/minimal-app/apps/webapp/src/modules/posts/PostsRoute.tsx.tpl +307 -751
  21. package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostCard.tsx.tpl +138 -0
  22. package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostEditorModal.tsx.tpl +124 -0
  23. package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostActions.ts.tpl +116 -0
  24. package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsList.ts.tpl +92 -0
  25. package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsRoute.ts.tpl +13 -0
  26. package/templates/minimal-app/apps/webapp/src/modules/posts/posts.utils.ts.tpl +16 -0
  27. package/templates/minimal-app/apps/webapp/src/navigation/navigation.config.tsx.tpl +85 -0
  28. package/templates/minimal-app/apps/webapp/translations/en/blog-app.json.tpl +2 -0
  29. package/templates/minimal-app/pnpm-workspace.yaml.tpl +6 -5
@@ -1,153 +1,59 @@
1
- import { Chip } from "@heroui/react";
2
- import { buttonVariants } from "@heroui/styles";
3
- import { authClient } from "@m5kdev/frontend/modules/auth/auth.lib";
4
- import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
5
- import { useTheme } from "@m5kdev/web-ui/components/theme-provider";
6
- import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
7
- import {
8
- BookTextIcon,
9
- Building2Icon,
10
- LogOutIcon,
11
- MoonStarIcon,
12
- NotebookTextIcon,
13
- SettingsIcon,
14
- ShieldCheckIcon,
15
- SunMediumIcon,
16
- UserCogIcon,
17
- UsersIcon,
18
- } from "lucide-react";
19
- import { useTranslation } from "react-i18next";
20
- import { NavLink, Outlet } from "react-router";
21
- import { PushNotificationsPanel } from "./modules/notification/PushNotificationsPanel";
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
-
37
- export function Layout() {
38
- const { data: session } = useSession();
39
- const { theme, setTheme } = useTheme();
40
- const { t } = useTranslation("blog{{PACKAGE_SCOPE}}");
41
-
42
- const userName = session?.user?.name || session?.user?.email || "Editor";
43
- const userEmail = session?.user?.email || "";
44
-
45
- return (
46
- <div className="min-h-screen bg-canvas text-foreground">
47
- <div className="mx-auto flex min-h-screen max-w-[1600px] flex-col gap-6 px-4 py-4 lg:flex-row lg:px-6">
48
- <aside className="w-full rounded-[32px] border border-white/60 bg-white/70 p-5 shadow-[0_24px_60px_rgba(81,50,24,0.14)] backdrop-blur xl:w-[320px]">
49
- <div className="flex items-start justify-between gap-4">
50
- <div>
51
- <p className="text-[0.68rem] font-semibold uppercase tracking-[0.32em] text-amber-700/80">
52
- {t("layout.brand.eyebrow")}
53
- </p>
54
- <h1 className="mt-3 font-editorial text-4xl leading-none text-ink">{APP_NAME}</h1>
55
- <p className="mt-3 max-w-[22ch] text-sm leading-6 text-ink/70">
56
- {t("layout.brand.tagline")}
57
- </p>
58
- </div>
59
- <div className="rounded-full border border-amber-200 bg-amber-50/90 p-3 text-amber-700">
60
- <NotebookTextIcon className="h-5 w-5" />
61
- </div>
62
- </div>
63
-
64
- <div className="mt-8 rounded-[28px] border border-amber-200/70 bg-amber-50/85 p-4">
65
- <p className="text-[0.68rem] font-semibold uppercase tracking-[0.28em] text-amber-700/80">
66
- {t("layout.workspace.eyebrow")}
67
- </p>
68
- <p className="mt-3 font-editorial text-2xl leading-none text-ink">
69
- {t("layout.workspace.title")}
70
- </p>
71
- <p className="mt-3 text-sm leading-6 text-ink/75">{t("layout.workspace.body")}</p>
72
- <div className="mt-4 flex flex-wrap gap-2">
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>
75
- </div>
76
- </div>
77
-
78
- <PushNotificationsPanel />
79
-
80
- <nav className="mt-8 grid gap-2">
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
- ))}
95
- </nav>
96
-
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)]">
98
- <p className="text-xs uppercase tracking-[0.24em] text-stone-400">
99
- {t("layout.account.eyebrow")}
100
- </p>
101
- <p className="mt-3 text-lg font-semibold">{userName}</p>
102
- <p className="mt-1 text-sm text-stone-300">{userEmail}</p>
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")}
108
- >
109
- {theme === "dark" ? (
110
- <SunMediumIcon className="h-4 w-4" />
111
- ) : (
112
- <MoonStarIcon className="h-4 w-4" />
113
- )}
114
- {theme === "dark" ? t("layout.account.light") : t("layout.account.dark")}
115
- </button>
116
- <button
117
- type="button"
118
- className={buttonVariants({ variant: "danger", size: "sm" })}
119
- onClick={() => {
120
- authClient.signOut();
121
- window.location.assign("/login");
122
- }}
123
- >
124
- <LogOutIcon className="h-4 w-4" />
125
- {t("layout.account.signOut")}
126
- </button>
127
- </div>
128
- </div>
129
- </aside>
130
-
131
- <main className="flex min-h-[calc(100vh-2rem)] flex-1 flex-col rounded-[32px] border border-white/60 bg-white/72 shadow-[0_24px_60px_rgba(81,50,24,0.14)] backdrop-blur">
132
- <header className="flex flex-wrap items-center justify-between gap-4 border-b border-white/70 px-6 py-5">
133
- <div>
134
- <p className="text-[0.68rem] font-semibold uppercase tracking-[0.28em] text-emerald-800/70">
135
- {t("layout.header.eyebrow")}
136
- </p>
137
- <h2 className="mt-2 font-editorial text-4xl leading-none text-ink">
138
- {t("layout.header.title")}
139
- </h2>
140
- </div>
141
- <Chip className="border border-emerald-300 bg-emerald-50 px-4 py-5 text-emerald-900">
142
- {t("layout.header.runtime")}
143
- </Chip>
144
- </header>
145
-
146
- <div className="flex-1 px-4 py-4 sm:px-6 sm:py-6">
147
- <Outlet />
148
- </div>
149
- </main>
150
- </div>
151
- </div>
152
- );
153
- }
1
+ import { authClient } from "@m5kdev/frontend/modules/auth/auth.lib";
2
+ import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
3
+ import { AppShell } from "@m5kdev/web-ui/modules/app/components/AppShell";
4
+ import { AppSidebarContent } from "@m5kdev/web-ui/modules/app/components/AppSidebarContent";
5
+ import { AppSidebarHeader } from "@m5kdev/web-ui/modules/app/components/AppSidebarHeader";
6
+ import { AppSidebarUser } from "@m5kdev/web-ui/modules/app/components/AppSidebarUser";
7
+ import { AuthOrganizationSelect } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationSelect";
8
+ import { AuthUtilityImpersonationBanner } from "@m5kdev/web-ui/modules/auth/components/AuthUtilityImpersonationBanner";
9
+ import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
10
+ import { useState } from "react";
11
+ import { useTranslation } from "react-i18next";
12
+ import { useNavigate } from "react-router";
13
+ import { PushNotificationsPanel } from "./modules/notification/PushNotificationsPanel";
14
+ import { APP_ROUTES, buildNavigationItems } from "./navigation/navigation.config";
15
+
16
+ export function Layout() {
17
+ const { data: session } = useSession();
18
+ const { t } = useTranslation("blog{{PACKAGE_SCOPE}}");
19
+ const navigate = useNavigate();
20
+
21
+ // ephemeral UI state: which collapsible sidebar groups are open
22
+ const [navigationState, setNavigationState] = useState<Record<string, boolean>>({});
23
+
24
+ const onSignOut = () => {
25
+ authClient.signOut();
26
+ navigate("/login");
27
+ };
28
+
29
+ return (
30
+ <AppShell
31
+ header={<AuthUtilityImpersonationBanner />}
32
+ sidebar={{
33
+ header: <AppSidebarHeader logo={{ src: "/logo.svg", alt: APP_NAME }} title={APP_NAME} />,
34
+ content: (
35
+ <>
36
+ <div className="px-2 group-data-[collapsible=icon]:hidden">
37
+ <AuthOrganizationSelect />
38
+ </div>
39
+ <AppSidebarContent
40
+ navigationItems={buildNavigationItems(t)}
41
+ navigationState={navigationState}
42
+ onNavigationStateChange={setNavigationState}
43
+ />
44
+ <div className="mt-auto px-2 group-data-[collapsible=icon]:hidden">
45
+ <PushNotificationsPanel />
46
+ </div>
47
+ </>
48
+ ),
49
+ footer: (
50
+ <AppSidebarUser
51
+ user={session?.user}
52
+ onSignOut={onSignOut}
53
+ organizationSettingsPath={APP_ROUTES.organization.preferences}
54
+ />
55
+ ),
56
+ }}
57
+ />
58
+ );
59
+ }
@@ -1,111 +1,100 @@
1
- import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
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 { syncI18nLocale } from "@m5kdev/frontend/modules/app/utils/locale";
9
- import { AuthUserRouter } from "@m5kdev/web-ui/modules/auth/components/AuthUserRouter";
10
- import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
11
- import type { ReactNode } from "react";
12
- import { useTranslation } from "react-i18next";
13
- import { Navigate, Outlet, Route, Routes } from "react-router";
14
- import { z } from "zod";
15
- import { PostsRoute } from "@/modules/posts/PostsRoute";
16
- import { Layout } from "./Layout";
17
-
18
- const preferenceSchema = z.object({
19
- compactMode: z.boolean().optional(),
20
- postsPerPage: z.number().min(1).max(20).optional(),
21
- });
22
-
23
- const preferenceControls = {
24
- compactMode: {
25
- label: "Compact mode",
26
- element: "switch",
27
- },
28
- postsPerPage: {
29
- label: "Posts per page",
30
- element: "number",
31
- min: 1,
32
- max: 20,
33
- step: 1,
34
- },
35
- } as const;
36
-
37
- function ProtectedRoutes({ children }: { children?: ReactNode }) {
38
- const { data: session } = useSession();
39
-
40
- if (!session) {
41
- return <Navigate to="/login" replace />;
42
- }
43
-
44
- return children ?? <Outlet />;
45
- }
46
-
47
- function AuthHeader() {
48
- const { t } = useTranslation("blog{{PACKAGE_SCOPE}}");
49
-
50
- return (
51
- <div className="text-center">
52
- <p className="text-[0.68rem] font-semibold uppercase tracking-[0.3em] text-default-500">
53
- {t("auth.header.eyebrow")}
54
- </p>
55
- <h1 className="mt-3 font-editorial text-4xl text-ink">{APP_NAME}</h1>
56
- <p className="mt-3 text-sm text-default-600">{t("auth.header.tagline")}</p>
57
- </div>
58
- );
59
- }
60
-
61
- export function Router() {
62
- const isWaitlist = import.meta.env.VITE_ENABLE_WAITLIST === "true";
63
-
64
- return (
65
- <Routes>
66
- {AuthPublicRouter({
67
- header: <AuthHeader />,
68
- waitlist: isWaitlist,
69
- onLocaleChange: syncI18nLocale,
70
- })}
71
-
72
- <Route
73
- path="/organization/accept-invitation"
74
- element={<AuthOrganizationAcceptInvitationRoute />}
75
- />
76
-
77
- <Route element={<ProtectedRoutes />}>
78
- <Route path="/" element={<Layout />}>
79
- <Route index element={<Navigate to="/posts" replace />} />
80
- <Route path="posts" element={<PostsRoute />} />
81
- <Route
82
- path="organization/members"
83
- element={<AuthOrganizationMembersRoute />}
84
- />
85
- <Route
86
- path="organization/manage"
87
- element={<AuthOrganizationChildOrganizationsRoute />}
88
- />
89
- <Route
90
- path="organization/preferences"
91
- element={
92
- <AuthOrganizationPreferences
93
- schema={preferenceSchema}
94
- controls={preferenceControls}
95
- />
96
- }
97
- />
98
- {AuthUserRouter({
99
- schema: preferenceSchema,
100
- controls: preferenceControls,
101
- onLocaleChange: syncI18nLocale,
102
- })}
103
- {AuthAdminRouter({
104
- enableWaitlist: true,
105
- enableAccountClaimActions: true,
106
- })}
107
- </Route>
108
- </Route>
109
- </Routes>
110
- );
111
- }
1
+ import { syncI18nLocale } from "@m5kdev/frontend/modules/app/utils/locale";
2
+ import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
3
+ import { AuthAdminRouter } from "@m5kdev/web-ui/modules/auth/components/AuthAdminRouter";
4
+ import { AuthOrganizationRouter } from "@m5kdev/web-ui/modules/auth/components/AuthOrganizationRouter";
5
+ import { AuthPublicRouter } from "@m5kdev/web-ui/modules/auth/components/AuthPublicRouter";
6
+ import { AuthUserRouter } from "@m5kdev/web-ui/modules/auth/components/AuthUserRouter";
7
+ import { BillingBetaPage } from "@m5kdev/web-ui/modules/billing/components/BillingBetaPage";
8
+ import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
9
+ import type { ReactNode } from "react";
10
+ import { useTranslation } from "react-i18next";
11
+ import { Navigate, Outlet, Route, Routes } from "react-router";
12
+ import { z } from "zod";
13
+ import { PostsRoute } from "@/modules/posts/PostsRoute";
14
+ import { Layout } from "./Layout";
15
+ import { APP_ROUTES } from "./navigation/navigation.config";
16
+
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
+ const toRelativePath = (path: string): string => path.replace(/^\//, "");
37
+
38
+ function ProtectedRoutes({ children }: { children?: ReactNode }) {
39
+ const { data: session } = useSession();
40
+
41
+ if (!session) {
42
+ return <Navigate to="/login" replace />;
43
+ }
44
+
45
+ return children ?? <Outlet />;
46
+ }
47
+
48
+ function AuthHeader() {
49
+ const { t } = useTranslation("blog{{PACKAGE_SCOPE}}");
50
+
51
+ return (
52
+ <div className="text-center">
53
+ <p className="text-[0.68rem] font-semibold uppercase tracking-[0.3em] text-default-500">
54
+ {t("auth.header.eyebrow")}
55
+ </p>
56
+ <h1 className="mt-3 font-editorial text-4xl text-ink">{APP_NAME}</h1>
57
+ <p className="mt-3 text-sm text-default-600">{t("auth.header.tagline")}</p>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ export function Router() {
63
+ const isWaitlist = import.meta.env.VITE_ENABLE_WAITLIST === "true";
64
+
65
+ return (
66
+ <Routes>
67
+ {/* Built-in routers register as function calls so their <Route> elements
68
+ land directly in this tree. */}
69
+ {AuthPublicRouter({
70
+ header: <AuthHeader />,
71
+ waitlist: isWaitlist,
72
+ onLocaleChange: syncI18nLocale,
73
+ })}
74
+
75
+ <Route element={<ProtectedRoutes />}>
76
+ <Route path={APP_ROUTES.home} element={<Layout />}>
77
+ <Route index element={<Navigate to={APP_ROUTES.posts} replace />} />
78
+ <Route path={toRelativePath(APP_ROUTES.posts)} element={<PostsRoute />} />
79
+ <Route
80
+ path={toRelativePath(APP_ROUTES.billing)}
81
+ element={<BillingBetaPage appName={APP_NAME} />}
82
+ />
83
+ {AuthOrganizationRouter({
84
+ schema: preferenceSchema,
85
+ controls: preferenceControls,
86
+ })}
87
+ {AuthUserRouter({
88
+ schema: preferenceSchema,
89
+ controls: preferenceControls,
90
+ onLocaleChange: syncI18nLocale,
91
+ })}
92
+ {AuthAdminRouter({
93
+ enableWaitlist: true,
94
+ enableAccountClaimActions: true,
95
+ })}
96
+ </Route>
97
+ </Route>
98
+ </Routes>
99
+ );
100
+ }