create-m5kdev 0.25.1 → 0.25.3
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/frontend-component-guide.mdc +95 -0
- package/templates/minimal-app/.cursor/rules/frontend-data-guide.mdc +59 -0
- package/templates/minimal-app/.cursor/rules/frontend-form-guide.mdc +80 -0
- package/templates/minimal-app/.cursor/rules/frontend-hook-guide.mdc +79 -0
- package/templates/minimal-app/.cursor/rules/frontend-i18n-guide.mdc +45 -0
- package/templates/minimal-app/.cursor/rules/module-db-guide.mdc +1 -1
- package/templates/minimal-app/.cursor/rules/module-grants-guide.mdc +1 -1
- package/templates/minimal-app/.cursor/rules/module-module-guide.mdc +1 -1
- package/templates/minimal-app/.cursor/rules/module-repository-guide.mdc +1 -1
- package/templates/minimal-app/.cursor/rules/module-schema-guide.mdc +1 -1
- package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +1 -1
- package/templates/minimal-app/.cursor/rules/module-trpc-guide.mdc +1 -1
- package/templates/minimal-app/AGENTS.md.tpl +7 -0
- package/templates/minimal-app/apps/email/package.json.tpl +3 -3
- package/templates/minimal-app/apps/webapp/AGENTS.md.tpl +24 -18
- package/templates/minimal-app/apps/webapp/public/logo.svg +6 -0
- package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +59 -153
- package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +100 -111
- package/templates/minimal-app/apps/webapp/src/modules/posts/PostsRoute.tsx.tpl +307 -751
- package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostCard.tsx.tpl +138 -0
- package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostEditorModal.tsx.tpl +124 -0
- package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostActions.ts.tpl +116 -0
- package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsList.ts.tpl +92 -0
- package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsRoute.ts.tpl +13 -0
- package/templates/minimal-app/apps/webapp/src/modules/posts/posts.utils.ts.tpl +16 -0
- package/templates/minimal-app/apps/webapp/src/navigation/navigation.config.tsx.tpl +85 -0
- package/templates/minimal-app/apps/webapp/translations/en/blog-app.json.tpl +2 -0
- package/templates/minimal-app/pnpm-workspace.yaml.tpl +6 -5
|
@@ -1,153 +1,59 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
-
<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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
/>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
<Route
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
+
}
|