ai-forge-cli 0.4.2 → 0.4.6
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/dist/add-component-AQPCXQ4O.js +120 -0
- package/dist/{add-feature-2AR4DP7P.js → add-feature-VEY62Y5M.js} +37 -8
- package/dist/add-hook-OE4BKE6B.js +103 -0
- package/dist/{add-integration-QXB63S3V.js → add-integration-NJ56UXSY.js} +97 -27
- package/dist/add-layout-2KQPPTJX.js +104 -0
- package/dist/add-page-GIC2ZXJI.js +81 -0
- package/dist/add-util-T5JXAV4G.js +73 -0
- package/dist/{chunk-PIFX2L5H.js → chunk-MBF6K2AC.js} +1 -0
- package/dist/chunk-YMJTSRWM.js +49 -0
- package/dist/index.js +8 -3
- package/dist/{init-OYJP5QCZ.js → init-C4FFZDSP.js} +1 -1
- package/dist/templates/component/Component.tsx.hbs +11 -0
- package/dist/templates/feature/routes/$id.tsx.hbs +3 -13
- package/dist/templates/feature/routes/index.tsx.hbs +19 -9
- package/dist/templates/feature/src/components/Card.tsx.hbs +21 -0
- package/dist/templates/feature/src/components/Detail.tsx.hbs +28 -0
- package/dist/templates/feature/src/components/Form.tsx.hbs +46 -0
- package/dist/templates/feature/src/components/List.tsx.hbs +26 -0
- package/dist/templates/feature/src/components/index.ts.hbs +4 -4
- package/dist/templates/hook/feature-hook.ts.hbs +4 -0
- package/dist/templates/hook/global-hook.ts.hbs +11 -0
- package/dist/templates/init/claude.md.hbs +37 -11
- package/dist/templates/integration/auth/src/components/auth/AuthGuard.tsx.hbs +31 -0
- package/dist/templates/integration/auth/src/components/auth/LoginForm.tsx.hbs +83 -0
- package/dist/templates/integration/auth/src/components/auth/SignupForm.tsx.hbs +102 -0
- package/dist/templates/integration/auth/src/components/auth/UserMenu.tsx.hbs +36 -0
- package/dist/templates/integration/auth/src/components/auth/index.ts.hbs +4 -0
- package/dist/templates/integration/auth/src/routes/login.tsx.hbs +14 -0
- package/dist/templates/integration/auth/src/routes/signup.tsx.hbs +14 -0
- package/dist/templates/integration/storage/src/components/storage/FilePreview.tsx.hbs +18 -0
- package/dist/templates/integration/storage/src/components/storage/FileUpload.tsx.hbs +49 -0
- package/dist/templates/integration/storage/src/components/storage/index.ts.hbs +2 -0
- package/dist/templates/layout/auth/_layout.tsx.hbs +15 -0
- package/dist/templates/layout/auth/index.tsx.hbs +5 -0
- package/dist/templates/layout/base/_layout.tsx.hbs +14 -0
- package/dist/templates/layout/base/index.tsx.hbs +13 -0
- package/dist/templates/layout/dashboard/_layout.tsx.hbs +20 -0
- package/dist/templates/layout/dashboard/components/Header.tsx.hbs +12 -0
- package/dist/templates/layout/dashboard/components/Sidebar.tsx.hbs +39 -0
- package/dist/templates/layout/dashboard/components/index.ts.hbs +2 -0
- package/dist/templates/layout/dashboard/index.tsx.hbs +15 -0
- package/dist/templates/layout/marketing/_layout.tsx.hbs +39 -0
- package/dist/templates/layout/marketing/index.tsx.hbs +16 -0
- package/dist/templates/page/components/Content.tsx.hbs +8 -0
- package/dist/templates/page/components/index.ts.hbs +1 -0
- package/dist/templates/page/route.tsx.hbs +10 -0
- package/dist/templates/util/util.ts.hbs +7 -0
- package/package.json +1 -1
- package/templates/component/Component.tsx.hbs +11 -0
- package/templates/feature/routes/$id.tsx.hbs +3 -13
- package/templates/feature/routes/index.tsx.hbs +19 -9
- package/templates/feature/src/components/Card.tsx.hbs +21 -0
- package/templates/feature/src/components/Detail.tsx.hbs +28 -0
- package/templates/feature/src/components/Form.tsx.hbs +46 -0
- package/templates/feature/src/components/List.tsx.hbs +26 -0
- package/templates/feature/src/components/index.ts.hbs +4 -4
- package/templates/hook/feature-hook.ts.hbs +4 -0
- package/templates/hook/global-hook.ts.hbs +11 -0
- package/templates/init/claude.md.hbs +37 -11
- package/templates/integration/auth/src/components/auth/AuthGuard.tsx.hbs +31 -0
- package/templates/integration/auth/src/components/auth/LoginForm.tsx.hbs +83 -0
- package/templates/integration/auth/src/components/auth/SignupForm.tsx.hbs +102 -0
- package/templates/integration/auth/src/components/auth/UserMenu.tsx.hbs +36 -0
- package/templates/integration/auth/src/components/auth/index.ts.hbs +4 -0
- package/templates/integration/auth/src/routes/login.tsx.hbs +14 -0
- package/templates/integration/auth/src/routes/signup.tsx.hbs +14 -0
- package/templates/integration/storage/src/components/storage/FilePreview.tsx.hbs +18 -0
- package/templates/integration/storage/src/components/storage/FileUpload.tsx.hbs +49 -0
- package/templates/integration/storage/src/components/storage/index.ts.hbs +2 -0
- package/templates/layout/auth/_layout.tsx.hbs +15 -0
- package/templates/layout/auth/index.tsx.hbs +5 -0
- package/templates/layout/base/_layout.tsx.hbs +14 -0
- package/templates/layout/base/index.tsx.hbs +13 -0
- package/templates/layout/dashboard/_layout.tsx.hbs +20 -0
- package/templates/layout/dashboard/components/Header.tsx.hbs +12 -0
- package/templates/layout/dashboard/components/Sidebar.tsx.hbs +39 -0
- package/templates/layout/dashboard/components/index.ts.hbs +2 -0
- package/templates/layout/dashboard/index.tsx.hbs +15 -0
- package/templates/layout/marketing/_layout.tsx.hbs +39 -0
- package/templates/layout/marketing/index.tsx.hbs +16 -0
- package/templates/page/components/Content.tsx.hbs +8 -0
- package/templates/page/components/index.ts.hbs +1 -0
- package/templates/page/route.tsx.hbs +10 -0
- package/templates/util/util.ts.hbs +7 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
kebabCase,
|
|
4
|
+
pascalCase,
|
|
5
|
+
renderTemplate
|
|
6
|
+
} from "./chunk-MBF6K2AC.js";
|
|
7
|
+
import {
|
|
8
|
+
fileExists,
|
|
9
|
+
logger,
|
|
10
|
+
readFile,
|
|
11
|
+
writeFile
|
|
12
|
+
} from "./chunk-HL4K5AHI.js";
|
|
13
|
+
|
|
14
|
+
// src/commands/add-page.ts
|
|
15
|
+
import { defineCommand } from "citty";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
var add_page_default = defineCommand({
|
|
18
|
+
meta: {
|
|
19
|
+
name: "add:page",
|
|
20
|
+
description: "Create a non-feature page (about, pricing, settings)"
|
|
21
|
+
},
|
|
22
|
+
args: {
|
|
23
|
+
name: {
|
|
24
|
+
type: "positional",
|
|
25
|
+
description: "Page name or path (e.g., 'about' or 'settings/profile')",
|
|
26
|
+
required: true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
async run({ args }) {
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
const rawName = args.name;
|
|
32
|
+
const name = kebabCase(rawName);
|
|
33
|
+
const routePath = name.includes("/") ? name.replace(/\//g, ".") : name;
|
|
34
|
+
const componentFolder = name.includes("/") ? name.split("/")[0] : name;
|
|
35
|
+
const componentName = pascalCase(name.replace(/\//g, "-"));
|
|
36
|
+
const routeFile = join(cwd, "src/routes", `${routePath}.tsx`);
|
|
37
|
+
if (await fileExists(routeFile)) {
|
|
38
|
+
logger.error(`Page "${name}" already exists at ${routeFile}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
logger.blank();
|
|
42
|
+
const templateData = {
|
|
43
|
+
name,
|
|
44
|
+
routePath,
|
|
45
|
+
componentFolder,
|
|
46
|
+
componentName
|
|
47
|
+
};
|
|
48
|
+
const routeContent = renderTemplate("page/route.tsx.hbs", templateData);
|
|
49
|
+
await writeFile(routeFile, routeContent);
|
|
50
|
+
logger.success(`Created src/routes/${routePath}.tsx`);
|
|
51
|
+
const contentPath = join(cwd, "src/components", componentFolder, `${componentName}Content.tsx`);
|
|
52
|
+
const contentTemplate = renderTemplate("page/components/Content.tsx.hbs", templateData);
|
|
53
|
+
await writeFile(contentPath, contentTemplate);
|
|
54
|
+
logger.success(`Created src/components/${componentFolder}/${componentName}Content.tsx`);
|
|
55
|
+
const indexPath = join(cwd, "src/components", componentFolder, "index.ts");
|
|
56
|
+
const exportLine = `export { ${componentName}Content } from "./${componentName}Content";`;
|
|
57
|
+
if (await fileExists(indexPath)) {
|
|
58
|
+
const existing = await readFile(indexPath);
|
|
59
|
+
if (!existing.includes(exportLine)) {
|
|
60
|
+
await writeFile(indexPath, existing.trim() + "\n" + exportLine + "\n");
|
|
61
|
+
logger.success(`Updated src/components/${componentFolder}/index.ts`);
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
const indexContent = renderTemplate("page/components/index.ts.hbs", templateData);
|
|
65
|
+
await writeFile(indexPath, indexContent);
|
|
66
|
+
logger.success(`Created src/components/${componentFolder}/index.ts`);
|
|
67
|
+
}
|
|
68
|
+
logger.blank();
|
|
69
|
+
logger.log(` Page "${name}" created!`);
|
|
70
|
+
logger.blank();
|
|
71
|
+
logger.log(" Files:");
|
|
72
|
+
logger.log(` - src/routes/${routePath}.tsx`);
|
|
73
|
+
logger.log(` - src/components/${componentFolder}/${componentName}Content.tsx`);
|
|
74
|
+
logger.blank();
|
|
75
|
+
logger.log(` Visit: http://localhost:3000/${name.replace(/\./g, "/")}`);
|
|
76
|
+
logger.blank();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
export {
|
|
80
|
+
add_page_default as default
|
|
81
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
camelCase,
|
|
4
|
+
kebabCase,
|
|
5
|
+
renderTemplate
|
|
6
|
+
} from "./chunk-MBF6K2AC.js";
|
|
7
|
+
import {
|
|
8
|
+
fileExists,
|
|
9
|
+
logger,
|
|
10
|
+
readFile,
|
|
11
|
+
writeFile
|
|
12
|
+
} from "./chunk-HL4K5AHI.js";
|
|
13
|
+
|
|
14
|
+
// src/commands/add-util.ts
|
|
15
|
+
import { defineCommand } from "citty";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
var add_util_default = defineCommand({
|
|
18
|
+
meta: {
|
|
19
|
+
name: "add:util",
|
|
20
|
+
description: "Create a new utility function in src/lib"
|
|
21
|
+
},
|
|
22
|
+
args: {
|
|
23
|
+
name: {
|
|
24
|
+
type: "positional",
|
|
25
|
+
description: "Utility name (will be camelCased)",
|
|
26
|
+
required: true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
async run({ args }) {
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
const rawName = args.name;
|
|
32
|
+
const utilName = camelCase(rawName);
|
|
33
|
+
const fileName = kebabCase(rawName);
|
|
34
|
+
const libDir = join(cwd, "src/lib");
|
|
35
|
+
const utilPath = join(libDir, `${fileName}.ts`);
|
|
36
|
+
const indexPath = join(libDir, "index.ts");
|
|
37
|
+
if (await fileExists(utilPath)) {
|
|
38
|
+
logger.error(`Utility "${utilName}" already exists at src/lib/${fileName}.ts`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
logger.blank();
|
|
42
|
+
const templateData = {
|
|
43
|
+
utilName,
|
|
44
|
+
fileName
|
|
45
|
+
};
|
|
46
|
+
const content = renderTemplate("util/util.ts.hbs", templateData);
|
|
47
|
+
await writeFile(utilPath, content);
|
|
48
|
+
logger.success(`Created src/lib/${fileName}.ts`);
|
|
49
|
+
const exportLine = `export { ${utilName} } from "./${fileName}";`;
|
|
50
|
+
if (await fileExists(indexPath)) {
|
|
51
|
+
const existing = await readFile(indexPath);
|
|
52
|
+
if (!existing.includes(exportLine)) {
|
|
53
|
+
await writeFile(indexPath, existing.trim() + "\n" + exportLine + "\n");
|
|
54
|
+
logger.success("Updated src/lib/index.ts");
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
await writeFile(indexPath, exportLine + "\n");
|
|
58
|
+
logger.success("Created src/lib/index.ts");
|
|
59
|
+
}
|
|
60
|
+
logger.blank();
|
|
61
|
+
logger.log(` Utility "${utilName}" created!`);
|
|
62
|
+
logger.blank();
|
|
63
|
+
logger.log(" Location:");
|
|
64
|
+
logger.log(` src/lib/${fileName}.ts`);
|
|
65
|
+
logger.blank();
|
|
66
|
+
logger.log(" Import:");
|
|
67
|
+
logger.log(` import { ${utilName} } from "~/lib";`);
|
|
68
|
+
logger.blank();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
export {
|
|
72
|
+
add_util_default as default
|
|
73
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
logger
|
|
4
|
+
} from "./chunk-HL4K5AHI.js";
|
|
5
|
+
|
|
6
|
+
// src/utils/shadcn.ts
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
var SHADCN_DEPS = {
|
|
11
|
+
feature: ["button", "input", "label"],
|
|
12
|
+
auth: ["button", "input", "label"],
|
|
13
|
+
storage: ["button"],
|
|
14
|
+
dashboard: ["button", "avatar", "dropdown-menu", "sheet"],
|
|
15
|
+
marketing: ["button"]
|
|
16
|
+
};
|
|
17
|
+
function runCommand(cmd, args, cwd) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const proc = spawn(cmd, args, { cwd, stdio: "pipe" });
|
|
20
|
+
proc.on("close", (code) => {
|
|
21
|
+
if (code === 0) resolve();
|
|
22
|
+
else reject(new Error(`Command failed with code ${code}`));
|
|
23
|
+
});
|
|
24
|
+
proc.on("error", reject);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async function ensureShadcnComponents(cwd, type) {
|
|
28
|
+
const required = SHADCN_DEPS[type];
|
|
29
|
+
if (!required || required.length === 0) return;
|
|
30
|
+
const uiDir = join(cwd, "src/components/ui");
|
|
31
|
+
const missing = required.filter(
|
|
32
|
+
(component) => !existsSync(join(uiDir, `${component}.tsx`))
|
|
33
|
+
);
|
|
34
|
+
if (missing.length === 0) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const step = logger.step(`Installing shadcn components: ${missing.join(", ")}`);
|
|
38
|
+
try {
|
|
39
|
+
await runCommand("npx", ["shadcn@latest", "add", ...missing, "--yes"], cwd);
|
|
40
|
+
step.succeed("shadcn components installed");
|
|
41
|
+
} catch {
|
|
42
|
+
step.fail("Could not auto-install shadcn components");
|
|
43
|
+
logger.warn(`Run manually: npx shadcn@latest add ${missing.join(" ")}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
ensureShadcnComponents
|
|
49
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -19,9 +19,14 @@ var main = defineCommand({
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
subCommands: {
|
|
22
|
-
init: () => import("./init-
|
|
23
|
-
"add:feature": () => import("./add-feature-
|
|
24
|
-
"add:integration": () => import("./add-integration-
|
|
22
|
+
init: () => import("./init-C4FFZDSP.js").then((m) => m.default),
|
|
23
|
+
"add:feature": () => import("./add-feature-VEY62Y5M.js").then((m) => m.default),
|
|
24
|
+
"add:integration": () => import("./add-integration-NJ56UXSY.js").then((m) => m.default),
|
|
25
|
+
"add:page": () => import("./add-page-GIC2ZXJI.js").then((m) => m.default),
|
|
26
|
+
"add:layout": () => import("./add-layout-2KQPPTJX.js").then((m) => m.default),
|
|
27
|
+
"add:component": () => import("./add-component-AQPCXQ4O.js").then((m) => m.default),
|
|
28
|
+
"add:hook": () => import("./add-hook-OE4BKE6B.js").then((m) => m.default),
|
|
29
|
+
"add:util": () => import("./add-util-T5JXAV4G.js").then((m) => m.default),
|
|
25
30
|
check: () => import("./check-YMGJNKME.js").then((m) => m.default),
|
|
26
31
|
version: () => import("./version-VO3LHLDO.js").then((m) => m.default)
|
|
27
32
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
-
import {
|
|
2
|
+
import { {{pascalCase name}}Detail } from "~/features/{{camelCase name}}";
|
|
3
3
|
import type { Id } from "@convex/_generated/dataModel";
|
|
4
4
|
|
|
5
5
|
export const Route = createFileRoute("/{{kebabCase name}}/$id")({
|
|
@@ -8,20 +8,10 @@ export const Route = createFileRoute("/{{kebabCase name}}/$id")({
|
|
|
8
8
|
|
|
9
9
|
function {{pascalCase name}}DetailRoute() {
|
|
10
10
|
const { id } = Route.useParams();
|
|
11
|
-
const { item, isLoading } = use{{pascalCase name}}(id as Id<"{{camelCase name}}">);
|
|
12
|
-
|
|
13
|
-
if (isLoading) {
|
|
14
|
-
return <div>Loading...</div>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (!item) {
|
|
18
|
-
return <div>Not found</div>;
|
|
19
|
-
}
|
|
20
11
|
|
|
21
12
|
return (
|
|
22
|
-
<div>
|
|
23
|
-
<
|
|
24
|
-
{/* Build your UI here */}
|
|
13
|
+
<div className="container py-8">
|
|
14
|
+
<{{pascalCase name}}Detail id={id as Id<"{{camelCase name}}">} />
|
|
25
15
|
</div>
|
|
26
16
|
);
|
|
27
17
|
}
|
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
-
import {
|
|
2
|
+
import { {{pascalCase name}}List, {{pascalCase name}}Form } from "~/features/{{camelCase name}}";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button } from "~/components/ui/button";
|
|
3
5
|
|
|
4
6
|
export const Route = createFileRoute("/{{kebabCase name}}/")({
|
|
5
7
|
component: {{pascalCase name}}IndexRoute,
|
|
6
8
|
});
|
|
7
9
|
|
|
8
10
|
function {{pascalCase name}}IndexRoute() {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
if (isLoading) {
|
|
12
|
-
return <div>Loading...</div>;
|
|
13
|
-
}
|
|
11
|
+
const [showForm, setShowForm] = useState(false);
|
|
14
12
|
|
|
15
13
|
return (
|
|
16
|
-
<div>
|
|
17
|
-
<
|
|
18
|
-
|
|
14
|
+
<div className="container py-8 space-y-6">
|
|
15
|
+
<div className="flex items-center justify-between">
|
|
16
|
+
<h1 className="text-3xl font-bold">{{pascalCase name}}</h1>
|
|
17
|
+
<Button onClick={() => setShowForm(!showForm)}>
|
|
18
|
+
{showForm ? "Cancel" : "Create New"}
|
|
19
|
+
</Button>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
{showForm && (
|
|
23
|
+
<div className="rounded-lg border p-4">
|
|
24
|
+
<{{pascalCase name}}Form onSuccess={() => setShowForm(false)} />
|
|
25
|
+
</div>
|
|
26
|
+
)}
|
|
27
|
+
|
|
28
|
+
<{{pascalCase name}}List />
|
|
19
29
|
</div>
|
|
20
30
|
);
|
|
21
31
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router";
|
|
2
|
+
import type { Doc } from "@convex/_generated/dataModel";
|
|
3
|
+
|
|
4
|
+
interface {{pascalCase name}}CardProps {
|
|
5
|
+
{{camelCase name}}: Doc<"{{camelCase name}}">;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function {{pascalCase name}}Card({ {{camelCase name}} }: {{pascalCase name}}CardProps) {
|
|
9
|
+
return (
|
|
10
|
+
<Link
|
|
11
|
+
to="/{{kebabCase name}}/$id"
|
|
12
|
+
params=\{{ id: {{camelCase name}}._id }}
|
|
13
|
+
className="block rounded-lg border p-4 hover:border-foreground transition-colors"
|
|
14
|
+
>
|
|
15
|
+
<h3 className="font-medium">{{pascalCase name}}</h3>
|
|
16
|
+
<p className="text-sm text-muted-foreground">
|
|
17
|
+
Created {new Date({{camelCase name}}.createdAt).toLocaleDateString()}
|
|
18
|
+
</p>
|
|
19
|
+
</Link>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { use{{pascalCase name}} } from "../hooks";
|
|
2
|
+
import type { Id } from "@convex/_generated/dataModel";
|
|
3
|
+
|
|
4
|
+
interface {{pascalCase name}}DetailProps {
|
|
5
|
+
id: Id<"{{camelCase name}}">;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function {{pascalCase name}}Detail({ id }: {{pascalCase name}}DetailProps) {
|
|
9
|
+
const { item, isLoading } = use{{pascalCase name}}(id);
|
|
10
|
+
|
|
11
|
+
if (isLoading) {
|
|
12
|
+
return <div className="animate-pulse">Loading...</div>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!item) {
|
|
16
|
+
return <div>Not found</div>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="space-y-4">
|
|
21
|
+
<h1 className="text-2xl font-bold">{{pascalCase name}} Details</h1>
|
|
22
|
+
{/* Customize your detail view here */}
|
|
23
|
+
<pre className="rounded bg-muted p-4 text-sm overflow-auto">
|
|
24
|
+
{JSON.stringify(item, null, 2)}
|
|
25
|
+
</pre>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { use{{pascalCase name}}Mutations } from "../hooks";
|
|
3
|
+
import { Button } from "~/components/ui/button";
|
|
4
|
+
import { Input } from "~/components/ui/input";
|
|
5
|
+
import { Label } from "~/components/ui/label";
|
|
6
|
+
|
|
7
|
+
interface {{pascalCase name}}FormProps {
|
|
8
|
+
onSuccess?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function {{pascalCase name}}Form({ onSuccess }: {{pascalCase name}}FormProps) {
|
|
12
|
+
const { create } = use{{pascalCase name}}Mutations();
|
|
13
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
14
|
+
|
|
15
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
setIsLoading(true);
|
|
18
|
+
|
|
19
|
+
const formData = new FormData(e.currentTarget);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await create({
|
|
23
|
+
// Add your fields here from formData
|
|
24
|
+
// Example: name: formData.get("name") as string,
|
|
25
|
+
});
|
|
26
|
+
e.currentTarget.reset();
|
|
27
|
+
onSuccess?.();
|
|
28
|
+
} finally {
|
|
29
|
+
setIsLoading(false);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
35
|
+
{/* Add your form fields here */}
|
|
36
|
+
<div className="space-y-2">
|
|
37
|
+
<Label htmlFor="name">Name</Label>
|
|
38
|
+
<Input id="name" name="name" required />
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<Button type="submit" disabled={isLoading}>
|
|
42
|
+
{isLoading ? "Creating..." : "Create {{pascalCase name}}"}
|
|
43
|
+
</Button>
|
|
44
|
+
</form>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { use{{pascalCase name}}List } from "../hooks";
|
|
2
|
+
import { {{pascalCase name}}Card } from "./{{pascalCase name}}Card";
|
|
3
|
+
|
|
4
|
+
export function {{pascalCase name}}List() {
|
|
5
|
+
const { items, isLoading } = use{{pascalCase name}}List();
|
|
6
|
+
|
|
7
|
+
if (isLoading) {
|
|
8
|
+
return <div className="animate-pulse">Loading...</div>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (items.length === 0) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="text-center py-12 text-muted-foreground">
|
|
14
|
+
No {{name}} yet. Create your first one!
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
21
|
+
{items.map((item) => (
|
|
22
|
+
<{{pascalCase name}}Card key={item._id} {{camelCase name}}={item} />
|
|
23
|
+
))}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export { {{pascalCase name}}List } from "./{{pascalCase name}}List";
|
|
2
|
+
export { {{pascalCase name}}Card } from "./{{pascalCase name}}Card";
|
|
3
|
+
export { {{pascalCase name}}Form } from "./{{pascalCase name}}Form";
|
|
4
|
+
export { {{pascalCase name}}Detail } from "./{{pascalCase name}}Detail";
|
|
@@ -25,12 +25,35 @@ The ONLY route you may create directly is `src/routes/index.tsx` (homepage).
|
|
|
25
25
|
pnpm dev # Start dev server
|
|
26
26
|
npx convex dev # Start Convex backend
|
|
27
27
|
pnpm lint # Check with Biome
|
|
28
|
-
forge add:feature <name> # Scaffold a new feature
|
|
29
|
-
forge add:integration auth # Add Convex Auth
|
|
30
|
-
forge add:integration storage # Add Convex file storage
|
|
31
28
|
forge check # Validate architecture (run before finishing)
|
|
32
29
|
```
|
|
33
30
|
|
|
31
|
+
## Forge CLI Commands
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Features (vertical slice architecture)
|
|
35
|
+
forge add:feature <name> # Scaffold full-stack feature (components, hooks, routes, Convex backend)
|
|
36
|
+
|
|
37
|
+
# Pages & Layouts
|
|
38
|
+
forge add:page <name> # Create a non-feature page (about, pricing, settings/profile)
|
|
39
|
+
forge add:layout <name> # Create a layout with preset (--preset dashboard|auth|marketing)
|
|
40
|
+
|
|
41
|
+
# Components, Hooks & Utilities
|
|
42
|
+
forge add:component <name> # Create component in src/components/ui/
|
|
43
|
+
forge add:component <name> --feature <feature> # Add component to a feature
|
|
44
|
+
forge add:component <name> --page <page> # Add component to a page folder
|
|
45
|
+
forge add:component <name> --layout <layout> # Add component to a layout folder
|
|
46
|
+
|
|
47
|
+
forge add:hook <name> # Create hook in src/hooks/
|
|
48
|
+
forge add:hook <name> --feature <feature> # Append hook to feature's hooks.ts
|
|
49
|
+
|
|
50
|
+
forge add:util <name> # Create utility in src/lib/
|
|
51
|
+
|
|
52
|
+
# Integrations
|
|
53
|
+
forge add:integration auth # Add Convex Auth with password flow + UI
|
|
54
|
+
forge add:integration storage # Add Convex file storage + UI components
|
|
55
|
+
```
|
|
56
|
+
|
|
34
57
|
## Integrations
|
|
35
58
|
|
|
36
59
|
For infrastructure (not features), use `forge add:integration`:
|
|
@@ -44,9 +67,10 @@ These create files in `convex/` and `src/lib/` - NOT in features.
|
|
|
44
67
|
```
|
|
45
68
|
src/routes/ → Thin route files (import from features, no logic)
|
|
46
69
|
src/features/ → Feature code (components/, hooks.ts)
|
|
47
|
-
src/components/ui/ → shadcn primitives
|
|
48
|
-
src/components/ →
|
|
49
|
-
src/
|
|
70
|
+
src/components/ui/ → shadcn primitives + global custom components
|
|
71
|
+
src/components/ → Page and layout specific components
|
|
72
|
+
src/hooks/ → Global hooks (created via forge add:hook)
|
|
73
|
+
src/lib/ → Utilities (cn.ts, auth.ts, storage.ts)
|
|
50
74
|
convex/features/ → Backend (mirrors src/features/)
|
|
51
75
|
convex/lib/ → Shared backend utilities (storage.ts)
|
|
52
76
|
convex/auth.ts → Auth configuration (from forge add:integration auth)
|
|
@@ -54,12 +78,12 @@ convex/auth.ts → Auth configuration (from forge add:integration auth)
|
|
|
54
78
|
|
|
55
79
|
## Where Components Go
|
|
56
80
|
|
|
57
|
-
- `src/components/ui/*` → shadcn primitives (Button, Card, Dialog)
|
|
58
|
-
- `src/components
|
|
59
|
-
- `src/
|
|
81
|
+
- `src/components/ui/*` → shadcn primitives (Button, Card, Dialog) + global custom components
|
|
82
|
+
- `src/components/<page>/` → Page-specific components (created via `forge add:page` or `forge add:component --page`)
|
|
83
|
+
- `src/components/<layout>/` → Layout components (Sidebar, Header - created via `forge add:layout`)
|
|
84
|
+
- `src/features/<name>/components/` → Feature-specific components
|
|
60
85
|
|
|
61
|
-
|
|
62
|
-
If you need 3+ components for a page, create a feature with `forge add:feature`.
|
|
86
|
+
Use `forge add:component` with appropriate flags to scaffold components in the correct location.
|
|
63
87
|
|
|
64
88
|
## Rules
|
|
65
89
|
|
|
@@ -71,4 +95,6 @@ If you need 3+ components for a page, create a feature with `forge add:feature`.
|
|
|
71
95
|
|
|
72
96
|
- **Data/Mutations**: Use hooks from `src/features/<name>/hooks.ts`
|
|
73
97
|
- **UI Components**: Import from `~/components/ui/*` (shadcn)
|
|
98
|
+
- **Global Hooks**: Import from `~/hooks`
|
|
99
|
+
- **Utilities**: Import from `~/lib`
|
|
74
100
|
- **Styling**: Tailwind classes only
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useConvexAuth } from "convex/react";
|
|
4
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
5
|
+
import { useEffect } from "react";
|
|
6
|
+
|
|
7
|
+
interface AuthGuardProps {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
fallback?: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function AuthGuard({ children, fallback }: AuthGuardProps) {
|
|
13
|
+
const { isAuthenticated, isLoading } = useConvexAuth();
|
|
14
|
+
const navigate = useNavigate();
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!isLoading && !isAuthenticated) {
|
|
18
|
+
navigate({ to: "/login" });
|
|
19
|
+
}
|
|
20
|
+
}, [isAuthenticated, isLoading, navigate]);
|
|
21
|
+
|
|
22
|
+
if (isLoading) {
|
|
23
|
+
return fallback ?? <div className="flex h-screen items-center justify-center">Loading...</div>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!isAuthenticated) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return <>{children}</>;
|
|
31
|
+
}
|