rahman-resources 0.1.0 → 0.2.1
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/bin/cli.js +130 -43
- package/lib/manifest.json +389 -20
- package/package.json +3 -2
package/bin/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// rahman-resources —
|
|
2
|
+
// rahman-resources — installer for the Rahman kitab.
|
|
3
3
|
// Usage:
|
|
4
|
-
// npx rahman-resources add <
|
|
5
|
-
// npx rahman-resources list
|
|
6
|
-
// npx rahman-resources info <
|
|
4
|
+
// npx rahman-resources add <slug> [target-dir]
|
|
5
|
+
// npx rahman-resources list [layouts|recipes|features]
|
|
6
|
+
// npx rahman-resources info <slug>
|
|
7
7
|
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
@@ -18,8 +18,10 @@ const require = createRequire(import.meta.url);
|
|
|
18
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
19
|
const manifest = require(path.join(__dirname, "../lib/manifest.json"));
|
|
20
20
|
|
|
21
|
-
const REPO = "rahmanef63/resource-site";
|
|
22
|
-
const BRANCH = "main";
|
|
21
|
+
const REPO = manifest.repo ?? "rahmanef63/resource-site";
|
|
22
|
+
const BRANCH = manifest.branch ?? "main";
|
|
23
|
+
|
|
24
|
+
const KINDS = /** @type {const} */ (["layout", "recipe", "feature"]);
|
|
23
25
|
|
|
24
26
|
const [, , cmd, ...rest] = process.argv;
|
|
25
27
|
|
|
@@ -34,7 +36,7 @@ async function main() {
|
|
|
34
36
|
return runAdd(rest);
|
|
35
37
|
case "list":
|
|
36
38
|
case "ls":
|
|
37
|
-
return runList();
|
|
39
|
+
return runList(rest);
|
|
38
40
|
case "info":
|
|
39
41
|
return runInfo(rest);
|
|
40
42
|
case undefined:
|
|
@@ -42,6 +44,10 @@ async function main() {
|
|
|
42
44
|
case "--help":
|
|
43
45
|
case "help":
|
|
44
46
|
return printHelp();
|
|
47
|
+
case "-v":
|
|
48
|
+
case "--version":
|
|
49
|
+
case "version":
|
|
50
|
+
return printVersion();
|
|
45
51
|
default:
|
|
46
52
|
console.error(kleur.red(`Unknown command: ${cmd}`));
|
|
47
53
|
printHelp();
|
|
@@ -49,65 +55,112 @@ async function main() {
|
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
|
|
58
|
+
function printVersion() {
|
|
59
|
+
const pkg = require(path.join(__dirname, "../package.json"));
|
|
60
|
+
console.log(pkg.version);
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
function printHelp() {
|
|
53
64
|
console.log(`
|
|
54
|
-
${kleur.bold("rahman-resources")} — install templates
|
|
65
|
+
${kleur.bold("rahman-resources")} — install templates, recipes, and features
|
|
55
66
|
|
|
56
67
|
${kleur.bold("Usage:")}
|
|
57
|
-
npx rahman-resources add <
|
|
58
|
-
npx rahman-resources list
|
|
59
|
-
npx rahman-resources info <
|
|
68
|
+
npx rahman-resources add <slug> [target-dir]
|
|
69
|
+
npx rahman-resources list [layouts|recipes|features]
|
|
70
|
+
npx rahman-resources info <slug>
|
|
60
71
|
|
|
61
72
|
${kleur.bold("Examples:")}
|
|
62
|
-
npx rahman-resources add personal-brand-os my-app
|
|
63
|
-
|
|
73
|
+
npx rahman-resources add personal-brand-os my-app ${kleur.dim("# layout — pulls folders + installs deps")}
|
|
74
|
+
npx rahman-resources add ai-sdk-openrouter my-app ${kleur.dim("# feature — runs npm install")}
|
|
75
|
+
npx rahman-resources add block-editor ${kleur.dim("# recipe — prints code + source URL")}
|
|
76
|
+
npx rahman-resources list features
|
|
64
77
|
`);
|
|
65
78
|
}
|
|
66
79
|
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
);
|
|
80
|
+
function findEntry(slug) {
|
|
81
|
+
for (const kind of KINDS) {
|
|
82
|
+
const list = manifest[kind + "s"];
|
|
83
|
+
const e = list.find((x) => x.slug === slug);
|
|
84
|
+
if (e) return { kind, entry: e };
|
|
73
85
|
}
|
|
74
|
-
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function runList([filter]) {
|
|
90
|
+
const groups = filter ? [filter] : ["layouts", "recipes", "features"];
|
|
91
|
+
for (const g of groups) {
|
|
92
|
+
const list = manifest[g];
|
|
93
|
+
if (!list || list.length === 0) continue;
|
|
94
|
+
console.log(`\n${kleur.bold(g.toUpperCase())} ${kleur.dim(`(${list.length})`)}\n`);
|
|
95
|
+
for (const t of list) {
|
|
96
|
+
const cat = (t.category ?? t.source ?? "").padEnd(20).slice(0, 20);
|
|
97
|
+
console.log(
|
|
98
|
+
` ${kleur.cyan(t.slug.padEnd(30))} ${kleur.dim(cat)} ${t.title}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
console.log(`\nRun ${kleur.cyan("info <slug>")} for detail, ${kleur.cyan("add <slug> [target]")} to install.\n`);
|
|
75
103
|
}
|
|
76
104
|
|
|
77
105
|
function runInfo([slug]) {
|
|
78
|
-
if (!slug) throw new Error("Usage: rahman-resources info <
|
|
79
|
-
const
|
|
80
|
-
if (!
|
|
106
|
+
if (!slug) throw new Error("Usage: rahman-resources info <slug>");
|
|
107
|
+
const found = findEntry(slug);
|
|
108
|
+
if (!found) throw new Error(`Slug not found: ${slug}. Run 'list' to see all.`);
|
|
109
|
+
const { kind, entry: t } = found;
|
|
110
|
+
|
|
81
111
|
console.log(`
|
|
82
|
-
${kleur.bold(t.title)} ${kleur.dim(`[${
|
|
112
|
+
${kleur.bold(t.title)} ${kleur.dim(`[${kind}]`)} ${kleur.dim(t.category ?? "")}
|
|
83
113
|
|
|
84
114
|
${t.description}
|
|
115
|
+
`);
|
|
85
116
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
117
|
+
if (kind === "layout") {
|
|
118
|
+
console.log(`${kleur.bold("Pulls:")}`);
|
|
119
|
+
console.log(t.pullPaths.map((p) => ` · ${p}`).join("\n") || " (none)");
|
|
120
|
+
if (t.dependencies?.length) {
|
|
121
|
+
console.log(`\n${kleur.bold("Dependencies:")}`);
|
|
122
|
+
console.log(t.dependencies.map((d) => ` · ${d}`).join("\n"));
|
|
123
|
+
}
|
|
124
|
+
} else if (kind === "feature") {
|
|
125
|
+
console.log(`${kleur.bold("Install:")}\n ${t.install}`);
|
|
126
|
+
if (t.npmPackages?.length) {
|
|
127
|
+
console.log(`\n${kleur.bold("npm packages:")}`);
|
|
128
|
+
console.log(t.npmPackages.map((d) => ` · ${d}`).join("\n"));
|
|
129
|
+
}
|
|
130
|
+
} else if (kind === "recipe") {
|
|
131
|
+
console.log(`${kleur.bold("Files:")}`);
|
|
132
|
+
console.log(t.files.map((f) => ` · ${f}`).join("\n"));
|
|
133
|
+
}
|
|
91
134
|
|
|
92
|
-
${kleur.dim(`
|
|
93
|
-
`);
|
|
135
|
+
if (t.docsUrl) console.log(`\n${kleur.dim(`Docs: ${t.docsUrl}`)}`);
|
|
136
|
+
console.log(`${kleur.dim(`Source: ${t.source ?? "—"}`)}\n`);
|
|
94
137
|
}
|
|
95
138
|
|
|
96
139
|
async function runAdd([slug, targetArg = "."]) {
|
|
97
140
|
if (!slug) {
|
|
98
|
-
console.error(kleur.red("Missing
|
|
141
|
+
console.error(kleur.red("Missing slug."));
|
|
99
142
|
printHelp();
|
|
100
143
|
process.exit(1);
|
|
101
144
|
}
|
|
102
|
-
const
|
|
103
|
-
if (!
|
|
145
|
+
const found = findEntry(slug);
|
|
146
|
+
if (!found) {
|
|
104
147
|
throw new Error(
|
|
105
|
-
`
|
|
148
|
+
`"${slug}" not found. Run ${kleur.cyan("npx rahman-resources list")}.`,
|
|
106
149
|
);
|
|
107
150
|
}
|
|
151
|
+
const { kind, entry } = found;
|
|
108
152
|
const target = path.resolve(process.cwd(), targetArg);
|
|
109
153
|
|
|
154
|
+
if (kind === "layout") return addLayout(entry, target, targetArg);
|
|
155
|
+
if (kind === "feature") return addFeature(entry, target, targetArg);
|
|
156
|
+
if (kind === "recipe") return addRecipe(entry);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function addLayout(t, target, targetArg) {
|
|
110
160
|
console.log(kleur.bold(`\n→ Installing ${kleur.cyan(t.title)} into ${kleur.dim(target)}\n`));
|
|
161
|
+
if (!t.pullPaths || t.pullPaths.length === 0) {
|
|
162
|
+
throw new Error(`Layout "${t.slug}" has no valid pullPaths in manifest.`);
|
|
163
|
+
}
|
|
111
164
|
for (const p of t.pullPaths) {
|
|
112
165
|
const dest = path.join(target, p);
|
|
113
166
|
process.stdout.write(` pulling ${kleur.dim(p)} ... `);
|
|
@@ -119,19 +172,53 @@ async function runAdd([slug, targetArg = "."]) {
|
|
|
119
172
|
const pm = detectPM(target);
|
|
120
173
|
console.log(kleur.bold(`\n→ Installing dependencies via ${kleur.cyan(pm)}\n`));
|
|
121
174
|
if (!hasPackageJson(target)) {
|
|
122
|
-
console.log(
|
|
123
|
-
|
|
124
|
-
);
|
|
125
|
-
console.log(kleur.dim(` Run later: cd ${targetArg} && ${pm} add ${t.dependencies.join(" ")}`));
|
|
175
|
+
console.log(kleur.yellow(` ${target}/package.json not found — skipping install.`));
|
|
176
|
+
console.log(kleur.dim(` Run later: cd ${targetArg} && ${pm} ${pm === "npm" ? "install" : "add"} ${t.dependencies.join(" ")}`));
|
|
126
177
|
} else {
|
|
127
|
-
await
|
|
178
|
+
await runPM(pm, t.dependencies, target);
|
|
128
179
|
}
|
|
129
180
|
}
|
|
130
181
|
|
|
131
182
|
console.log(`\n${kleur.green("✓")} Done. ${kleur.bold(t.title)} installed.`);
|
|
132
|
-
if (t.agentRecipe) {
|
|
133
|
-
|
|
183
|
+
if (t.agentRecipe) console.log(`\n${kleur.bold("Next:")}\n${indent(t.agentRecipe, 2)}\n`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function addFeature(t, target, targetArg) {
|
|
187
|
+
console.log(kleur.bold(`\n→ Adding feature ${kleur.cyan(t.title)} to ${kleur.dim(target)}\n`));
|
|
188
|
+
if (!t.npmPackages || t.npmPackages.length === 0) {
|
|
189
|
+
console.log(kleur.dim(` No npm packages to install (${t.install}).`));
|
|
190
|
+
} else {
|
|
191
|
+
const pm = detectPM(target);
|
|
192
|
+
if (!hasPackageJson(target)) {
|
|
193
|
+
console.log(kleur.yellow(` ${target}/package.json not found — skipping install.`));
|
|
194
|
+
console.log(kleur.dim(` Run later: cd ${targetArg} && ${pm} ${pm === "npm" ? "install" : "add"} ${t.npmPackages.join(" ")}`));
|
|
195
|
+
} else {
|
|
196
|
+
console.log(kleur.dim(` via ${pm}: ${t.npmPackages.join(" ")}\n`));
|
|
197
|
+
await runPM(pm, t.npmPackages, target);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log(`\n${kleur.green("✓")} Feature added: ${kleur.bold(t.title)}`);
|
|
202
|
+
if (t.exampleCode) {
|
|
203
|
+
console.log(`\n${kleur.bold("Example:")}`);
|
|
204
|
+
console.log(indent(t.exampleCode, 2));
|
|
205
|
+
}
|
|
206
|
+
if (t.agentRecipe) console.log(`\n${kleur.bold("Wire-up:")}\n${indent(t.agentRecipe, 2)}\n`);
|
|
207
|
+
if (t.docsUrl) console.log(`\n${kleur.dim(`Docs: ${t.docsUrl}`)}\n`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function addRecipe(t) {
|
|
211
|
+
console.log(`\n${kleur.bold(t.title)} ${kleur.dim("(recipe — manual port)")}\n`);
|
|
212
|
+
console.log(t.description);
|
|
213
|
+
console.log(`\n${kleur.bold("Source:")} ${t.source}`);
|
|
214
|
+
console.log(`\n${kleur.bold("Files to port:")}`);
|
|
215
|
+
console.log(t.files.map((f) => ` · ${f}`).join("\n"));
|
|
216
|
+
if (t.exampleCode) {
|
|
217
|
+
console.log(`\n${kleur.bold("Example:")}`);
|
|
218
|
+
console.log(indent(t.exampleCode, 2));
|
|
134
219
|
}
|
|
220
|
+
if (t.agentRecipe) console.log(`\n${kleur.bold("Wire-up:")}\n${indent(t.agentRecipe, 2)}\n`);
|
|
221
|
+
console.log(kleur.dim(`\n(Recipes are educational patterns — copy from source repo into your project manually.)\n`));
|
|
135
222
|
}
|
|
136
223
|
|
|
137
224
|
async function pull(repoPath, dest) {
|
|
@@ -154,7 +241,7 @@ function hasPackageJson(target) {
|
|
|
154
241
|
return existsSync(path.join(target, "package.json"));
|
|
155
242
|
}
|
|
156
243
|
|
|
157
|
-
function
|
|
244
|
+
function runPM(pm, deps, cwd) {
|
|
158
245
|
const args = pm === "npm" ? ["install", ...deps] : ["add", ...deps];
|
|
159
246
|
return new Promise((resolve, reject) => {
|
|
160
247
|
const ps = spawn(pm, args, { cwd, stdio: "inherit", shell: true });
|
package/lib/manifest.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"version": 2,
|
|
3
|
+
"generatedAt": "2026-05-05T11:50:32.844Z",
|
|
3
4
|
"repo": "rahmanef63/resource-site",
|
|
4
5
|
"branch": "main",
|
|
5
|
-
"
|
|
6
|
+
"layouts": [
|
|
6
7
|
{
|
|
7
8
|
"slug": "personal-brand-os",
|
|
8
9
|
"title": "Personal Brand OS",
|
|
@@ -16,6 +17,9 @@
|
|
|
16
17
|
"convex/templates/personal-brand-os"
|
|
17
18
|
],
|
|
18
19
|
"files": [
|
|
20
|
+
"app/preview/personal-brand-os/robots.ts",
|
|
21
|
+
"app/preview/personal-brand-os/sitemap.ts",
|
|
22
|
+
"app/preview/personal-brand-os/opengraph-image.tsx",
|
|
19
23
|
"app/preview/personal-brand-os/public/layout.tsx",
|
|
20
24
|
"app/preview/personal-brand-os/public/page.tsx",
|
|
21
25
|
"app/preview/personal-brand-os/public/blog/page.tsx",
|
|
@@ -32,17 +36,51 @@
|
|
|
32
36
|
"app/preview/personal-brand-os/admin/posts/new/page.tsx",
|
|
33
37
|
"app/preview/personal-brand-os/admin/posts/[id]/page.tsx",
|
|
34
38
|
"app/preview/personal-brand-os/admin/portfolio/page.tsx",
|
|
39
|
+
"app/preview/personal-brand-os/admin/portfolio/new/page.tsx",
|
|
40
|
+
"app/preview/personal-brand-os/admin/portfolio/[id]/page.tsx",
|
|
41
|
+
"app/preview/personal-brand-os/admin/services/page.tsx",
|
|
42
|
+
"app/preview/personal-brand-os/admin/resources/page.tsx",
|
|
35
43
|
"app/preview/personal-brand-os/admin/leads/page.tsx",
|
|
36
44
|
"app/preview/personal-brand-os/admin/comments/page.tsx",
|
|
37
45
|
"app/preview/personal-brand-os/admin/chatbot/page.tsx",
|
|
46
|
+
"app/preview/personal-brand-os/admin/newsletter/page.tsx",
|
|
47
|
+
"app/preview/personal-brand-os/admin/analytics/page.tsx",
|
|
48
|
+
"app/preview/personal-brand-os/admin/settings/site/page.tsx",
|
|
49
|
+
"app/preview/personal-brand-os/admin/settings/team/page.tsx",
|
|
50
|
+
"app/preview/personal-brand-os/admin/settings/ai/page.tsx",
|
|
38
51
|
"components/templates/personal-brand/shared/types.ts",
|
|
39
52
|
"components/templates/personal-brand/shared/store.tsx",
|
|
40
53
|
"components/templates/personal-brand/shared/seed.ts",
|
|
54
|
+
"components/templates/personal-brand/shared/site-config.ts",
|
|
41
55
|
"components/templates/personal-brand/shared/ui/site-nav.tsx",
|
|
42
56
|
"components/templates/personal-brand/shared/ui/site-footer.tsx",
|
|
43
57
|
"components/templates/personal-brand/shared/ui/chat-fab.tsx",
|
|
58
|
+
"components/templates/personal-brand/shared/ui/section-head.tsx",
|
|
59
|
+
"components/templates/personal-brand/slices/home/HomePage.tsx",
|
|
60
|
+
"components/templates/personal-brand/slices/home/NewsletterBlock.tsx",
|
|
61
|
+
"components/templates/personal-brand/slices/blog/BlogList.tsx",
|
|
62
|
+
"components/templates/personal-brand/slices/blog/BlogDetail.tsx",
|
|
63
|
+
"components/templates/personal-brand/slices/portfolio/PortfolioListPage.tsx",
|
|
64
|
+
"components/templates/personal-brand/slices/portfolio/PortfolioDetailPage.tsx",
|
|
65
|
+
"components/templates/personal-brand/slices/services/ServicesPage.tsx",
|
|
66
|
+
"components/templates/personal-brand/slices/resources/ResourcesPage.tsx",
|
|
67
|
+
"components/templates/personal-brand/slices/about/AboutPage.tsx",
|
|
68
|
+
"components/templates/personal-brand/slices/contact/ContactPage.tsx",
|
|
44
69
|
"components/templates/personal-brand/slices/admin/shell/admin-sidebar.tsx",
|
|
45
70
|
"components/templates/personal-brand/slices/admin/shell/admin-topbar.tsx",
|
|
71
|
+
"components/templates/personal-brand/slices/admin/dashboard/DashboardView.tsx",
|
|
72
|
+
"components/templates/personal-brand/slices/admin/posts/PostsList.tsx",
|
|
73
|
+
"components/templates/personal-brand/slices/admin/posts/PostEditor.tsx",
|
|
74
|
+
"components/templates/personal-brand/slices/admin/portfolio/PortfolioListAdmin.tsx",
|
|
75
|
+
"components/templates/personal-brand/slices/admin/portfolio/PortfolioEditor.tsx",
|
|
76
|
+
"components/templates/personal-brand/slices/admin/services/ServicesAdminView.tsx",
|
|
77
|
+
"components/templates/personal-brand/slices/admin/resources/ResourcesAdminView.tsx",
|
|
78
|
+
"components/templates/personal-brand/slices/admin/leads/LeadsView.tsx",
|
|
79
|
+
"components/templates/personal-brand/slices/admin/comments/CommentsView.tsx",
|
|
80
|
+
"components/templates/personal-brand/slices/admin/chatbot/ChatbotAdminView.tsx",
|
|
81
|
+
"components/templates/personal-brand/slices/admin/newsletter/NewsletterView.tsx",
|
|
82
|
+
"components/templates/personal-brand/slices/admin/analytics/AnalyticsView.tsx",
|
|
83
|
+
"components/templates/personal-brand/slices/admin/settings/SettingsView.tsx",
|
|
46
84
|
"convex/templates/personal-brand-os/schema.ts",
|
|
47
85
|
"convex/templates/personal-brand-os/posts.ts",
|
|
48
86
|
"convex/templates/personal-brand-os/portfolio.ts",
|
|
@@ -64,9 +102,18 @@
|
|
|
64
102
|
"next-themes",
|
|
65
103
|
"tailwindcss@^4",
|
|
66
104
|
"convex",
|
|
67
|
-
"@convex-dev/auth"
|
|
105
|
+
"@convex-dev/auth",
|
|
106
|
+
"@radix-ui/react-avatar",
|
|
107
|
+
"@radix-ui/react-dialog",
|
|
108
|
+
"@radix-ui/react-dropdown-menu",
|
|
109
|
+
"@radix-ui/react-label",
|
|
110
|
+
"@radix-ui/react-scroll-area",
|
|
111
|
+
"@radix-ui/react-select",
|
|
112
|
+
"@radix-ui/react-separator",
|
|
113
|
+
"@radix-ui/react-slot",
|
|
114
|
+
"@radix-ui/react-tabs"
|
|
68
115
|
],
|
|
69
|
-
"agentRecipe": "Personal Brand OS = full-app template (public + admin).
|
|
116
|
+
"agentRecipe": "Personal Brand OS = full-app template (public + admin). 1) Move app/preview/personal-brand-os/{robots,sitemap,opengraph-image}.* to app root. 2) Copy app/preview/personal-brand-os/public into app/(public)/, app/preview/personal-brand-os/admin into app/(admin)/. 3) Edit components/templates/personal-brand/shared/site-config.ts — set brandName, ownerName, baseUrl, twitter, email. 4) Wire convex/templates/personal-brand-os/* to convex/_generated and add @convex-dev/auth on admin routes. 5) Replace localStorage StoreProvider with Convex queries (schema mirrors localStorage shape).",
|
|
70
117
|
"tags": [
|
|
71
118
|
"template",
|
|
72
119
|
"personal-brand",
|
|
@@ -74,7 +121,8 @@
|
|
|
74
121
|
"portfolio",
|
|
75
122
|
"admin",
|
|
76
123
|
"saas"
|
|
77
|
-
]
|
|
124
|
+
],
|
|
125
|
+
"primaryFile": "app/preview/personal-brand-os/public/page.tsx"
|
|
78
126
|
},
|
|
79
127
|
{
|
|
80
128
|
"slug": "landing-hero-carousel",
|
|
@@ -84,7 +132,7 @@
|
|
|
84
132
|
"source": "cescadesigns",
|
|
85
133
|
"repoPath": "cookbook/layouts/landing-hero-carousel",
|
|
86
134
|
"pullPaths": [
|
|
87
|
-
"
|
|
135
|
+
"app/preview/landing-hero-carousel"
|
|
88
136
|
],
|
|
89
137
|
"files": [],
|
|
90
138
|
"dependencies": [],
|
|
@@ -94,7 +142,8 @@
|
|
|
94
142
|
"carousel",
|
|
95
143
|
"image",
|
|
96
144
|
"cms"
|
|
97
|
-
]
|
|
145
|
+
],
|
|
146
|
+
"primaryFile": "src/HeroSection.tsx"
|
|
98
147
|
},
|
|
99
148
|
{
|
|
100
149
|
"slug": "landing-asymmetric-masonry",
|
|
@@ -104,7 +153,7 @@
|
|
|
104
153
|
"source": "rahmanef.com",
|
|
105
154
|
"repoPath": "cookbook/layouts/landing-asymmetric-masonry",
|
|
106
155
|
"pullPaths": [
|
|
107
|
-
"
|
|
156
|
+
"app/preview/landing-asymmetric-masonry"
|
|
108
157
|
],
|
|
109
158
|
"files": [],
|
|
110
159
|
"dependencies": [],
|
|
@@ -114,7 +163,8 @@
|
|
|
114
163
|
"portfolio",
|
|
115
164
|
"masonry",
|
|
116
165
|
"scroll-reveal"
|
|
117
|
-
]
|
|
166
|
+
],
|
|
167
|
+
"primaryFile": "src/PortfolioGrid.tsx"
|
|
118
168
|
},
|
|
119
169
|
{
|
|
120
170
|
"slug": "landing-bento",
|
|
@@ -124,7 +174,7 @@
|
|
|
124
174
|
"source": "synthesized",
|
|
125
175
|
"repoPath": "cookbook/layouts/landing-bento",
|
|
126
176
|
"pullPaths": [
|
|
127
|
-
"
|
|
177
|
+
"app/preview/landing-bento"
|
|
128
178
|
],
|
|
129
179
|
"files": [],
|
|
130
180
|
"dependencies": [],
|
|
@@ -133,7 +183,8 @@
|
|
|
133
183
|
"marketing",
|
|
134
184
|
"bento",
|
|
135
185
|
"features"
|
|
136
|
-
]
|
|
186
|
+
],
|
|
187
|
+
"primaryFile": "README.md"
|
|
137
188
|
},
|
|
138
189
|
{
|
|
139
190
|
"slug": "landing-kinetic-text",
|
|
@@ -143,7 +194,7 @@
|
|
|
143
194
|
"source": "rahmanef.com",
|
|
144
195
|
"repoPath": "cookbook/layouts/landing-kinetic-text",
|
|
145
196
|
"pullPaths": [
|
|
146
|
-
"
|
|
197
|
+
"app/preview/landing-kinetic-text"
|
|
147
198
|
],
|
|
148
199
|
"files": [],
|
|
149
200
|
"dependencies": [],
|
|
@@ -152,7 +203,8 @@
|
|
|
152
203
|
"marketing",
|
|
153
204
|
"motion",
|
|
154
205
|
"type"
|
|
155
|
-
]
|
|
206
|
+
],
|
|
207
|
+
"primaryFile": "README.md"
|
|
156
208
|
},
|
|
157
209
|
{
|
|
158
210
|
"slug": "dashboard-three-column",
|
|
@@ -162,7 +214,7 @@
|
|
|
162
214
|
"source": "kitab-core",
|
|
163
215
|
"repoPath": "cookbook/layouts/dashboard-three-column",
|
|
164
216
|
"pullPaths": [
|
|
165
|
-
"
|
|
217
|
+
"app/preview/dashboard-three-column"
|
|
166
218
|
],
|
|
167
219
|
"files": [],
|
|
168
220
|
"dependencies": [],
|
|
@@ -172,7 +224,8 @@
|
|
|
172
224
|
"three-column",
|
|
173
225
|
"resizable",
|
|
174
226
|
"responsive"
|
|
175
|
-
]
|
|
227
|
+
],
|
|
228
|
+
"primaryFile": "template-base/frontend/shared/ui/layout/container/three-column/ThreeColumnLayout.tsx"
|
|
176
229
|
},
|
|
177
230
|
{
|
|
178
231
|
"slug": "dashboard-ide",
|
|
@@ -182,7 +235,7 @@
|
|
|
182
235
|
"source": "synthesized",
|
|
183
236
|
"repoPath": "cookbook/layouts/dashboard-ide",
|
|
184
237
|
"pullPaths": [
|
|
185
|
-
"
|
|
238
|
+
"app/preview/dashboard-ide"
|
|
186
239
|
],
|
|
187
240
|
"files": [],
|
|
188
241
|
"dependencies": [],
|
|
@@ -191,7 +244,8 @@
|
|
|
191
244
|
"dashboard",
|
|
192
245
|
"ide",
|
|
193
246
|
"editor"
|
|
194
|
-
]
|
|
247
|
+
],
|
|
248
|
+
"primaryFile": "README.md"
|
|
195
249
|
},
|
|
196
250
|
{
|
|
197
251
|
"slug": "dashboard-mobile-dock",
|
|
@@ -201,7 +255,7 @@
|
|
|
201
255
|
"source": "kitab-core",
|
|
202
256
|
"repoPath": "cookbook/layouts/dashboard-mobile-dock",
|
|
203
257
|
"pullPaths": [
|
|
204
|
-
"
|
|
258
|
+
"app/preview/dashboard-mobile-dock"
|
|
205
259
|
],
|
|
206
260
|
"files": [],
|
|
207
261
|
"dependencies": [],
|
|
@@ -210,7 +264,8 @@
|
|
|
210
264
|
"dashboard",
|
|
211
265
|
"mobile",
|
|
212
266
|
"pwa"
|
|
213
|
-
]
|
|
267
|
+
],
|
|
268
|
+
"primaryFile": "template-base/frontend/shared/ui/layout/dashboard/MobileDashboardShell.tsx"
|
|
214
269
|
},
|
|
215
270
|
{
|
|
216
271
|
"slug": "cms-public-storefront",
|
|
@@ -220,7 +275,7 @@
|
|
|
220
275
|
"source": "kitab-core cms-lite",
|
|
221
276
|
"repoPath": "cookbook/layouts/cms-public-storefront",
|
|
222
277
|
"pullPaths": [
|
|
223
|
-
"
|
|
278
|
+
"app/preview/cms-public-storefront"
|
|
224
279
|
],
|
|
225
280
|
"files": [],
|
|
226
281
|
"dependencies": [],
|
|
@@ -229,6 +284,320 @@
|
|
|
229
284
|
"cms",
|
|
230
285
|
"ecommerce",
|
|
231
286
|
"storefront"
|
|
287
|
+
],
|
|
288
|
+
"primaryFile": "README.md"
|
|
289
|
+
}
|
|
290
|
+
],
|
|
291
|
+
"recipes": [
|
|
292
|
+
{
|
|
293
|
+
"slug": "block-editor",
|
|
294
|
+
"title": "Notion-Style Block Editor",
|
|
295
|
+
"description": "21-block contenteditable editor with slash command menu, markdown shortcuts, drag handles. Real-time via Convex.",
|
|
296
|
+
"source": "notion-page-clone",
|
|
297
|
+
"files": [
|
|
298
|
+
"frontend/slices/notion/slices/editor/BlockEditor.tsx",
|
|
299
|
+
"frontend/slices/notion/slices/editor/SlashMenu.tsx",
|
|
300
|
+
"frontend/slices/notion/slices/editor/blockSpecs.ts"
|
|
301
|
+
],
|
|
302
|
+
"exampleCode": "import { BlockEditor } from \"@/frontend/slices/notion/slices/editor/BlockEditor\";\n\n<BlockEditor pageId={pageId} />",
|
|
303
|
+
"agentRecipe": "Already copied at frontend/slices/notion/slices/editor/. See PORT-NOTION.md for Vite→Next port checklist (routing rewrite, use-client markers, Convex API surface rename).",
|
|
304
|
+
"tags": [
|
|
305
|
+
"editor",
|
|
306
|
+
"notion",
|
|
307
|
+
"blocks",
|
|
308
|
+
"real-time"
|
|
309
|
+
]
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
"slug": "page-tree-sidebar",
|
|
313
|
+
"title": "Page Tree Sidebar",
|
|
314
|
+
"description": "Hierarchical workspace sidebar with @dnd-kit drag-drop reordering, favorites, recents.",
|
|
315
|
+
"source": "notion-page-clone",
|
|
316
|
+
"files": [
|
|
317
|
+
"frontend/slices/notion/slices/workspace-sidebar/components/WorkspaceSidebar.tsx",
|
|
318
|
+
"frontend/slices/notion/slices/workspace-sidebar/components/SortablePageRow.tsx"
|
|
319
|
+
],
|
|
320
|
+
"exampleCode": "import { WorkspaceSidebar } from \"@/frontend/slices/notion/slices/workspace-sidebar/components/WorkspaceSidebar\";\n\n<WorkspaceSidebar />",
|
|
321
|
+
"agentRecipe": "Mount WorkspaceSidebar inside the left slot of <ThreeColumnLayout>. State backed by Zustand store at frontend/slices/notion/shared/lib/store.tsx.",
|
|
322
|
+
"tags": [
|
|
323
|
+
"sidebar",
|
|
324
|
+
"tree",
|
|
325
|
+
"dnd-kit",
|
|
326
|
+
"navigation"
|
|
327
|
+
]
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"slug": "multi-block-selection",
|
|
331
|
+
"title": "Multi-Block Selection",
|
|
332
|
+
"description": "Marquee + click+shift selection of editor blocks with floating toolbar. Bulk actions: delete, duplicate, convert.",
|
|
333
|
+
"source": "notion-page-clone",
|
|
334
|
+
"files": [
|
|
335
|
+
"frontend/slices/notion/slices/block-selection/components/BlockSelectionProvider.tsx",
|
|
336
|
+
"frontend/slices/notion/slices/block-selection/components/MarqueeOverlay.tsx"
|
|
337
|
+
],
|
|
338
|
+
"exampleCode": "import { BlockSelectionProvider } from \"@/frontend/slices/notion/slices/block-selection/components/BlockSelectionProvider\";\n\n<BlockSelectionProvider>\n <BlockEditor />\n</BlockSelectionProvider>",
|
|
339
|
+
"agentRecipe": "Wrap BlockEditor with BlockSelectionProvider. The marquee overlay attaches to document; toolbar floats above the selection bounding box.",
|
|
340
|
+
"tags": [
|
|
341
|
+
"selection",
|
|
342
|
+
"editor",
|
|
343
|
+
"bulk",
|
|
344
|
+
"marquee"
|
|
345
|
+
]
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"slug": "database-views",
|
|
349
|
+
"title": "Database Views (11 types)",
|
|
350
|
+
"description": "Properties+rows database with 11 view types: table, board, calendar, timeline, chart, gallery, map. Per-view filter/sort/group.",
|
|
351
|
+
"source": "notion-page-clone",
|
|
352
|
+
"files": [
|
|
353
|
+
"frontend/slices/notion/slices/databases/DatabaseBlock.tsx",
|
|
354
|
+
"frontend/slices/notion/slices/databases/views/TableView.tsx",
|
|
355
|
+
"frontend/slices/notion/slices/databases/views/BoardView.tsx"
|
|
356
|
+
],
|
|
357
|
+
"exampleCode": "import { DatabaseBlock } from \"@/frontend/slices/notion/slices/databases/DatabaseBlock\";\n\n<DatabaseBlock databaseId={dbId} />",
|
|
358
|
+
"agentRecipe": "DatabaseBlock auto-routes to the active view component. Add custom property types by extending PropertyCell.tsx.",
|
|
359
|
+
"tags": [
|
|
360
|
+
"database",
|
|
361
|
+
"views",
|
|
362
|
+
"table",
|
|
363
|
+
"kanban",
|
|
364
|
+
"calendar"
|
|
365
|
+
]
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
"slug": "command-palette",
|
|
369
|
+
"title": "Command Palette (⌘K)",
|
|
370
|
+
"description": "Cmd+K modal: feature navigation, workspace switching, theme, sign-out, custom commands. Auto-builds from feature registry.",
|
|
371
|
+
"source": "kitab-core + notion-page-clone",
|
|
372
|
+
"files": [
|
|
373
|
+
"frontend/shared/foundation/utils/system/command-menu/components.tsx"
|
|
374
|
+
],
|
|
375
|
+
"exampleCode": "import { CommandMenu } from \"@/frontend/shared/foundation/utils/system/command-menu/components\";\n\n<CommandMenu actions={customActions} />",
|
|
376
|
+
"agentRecipe": "Mount CommandMenu once at the app shell level. It listens for Cmd+K globally. Pass extra commands via the actions prop or register via the command-registry.",
|
|
377
|
+
"tags": [
|
|
378
|
+
"palette",
|
|
379
|
+
"cmd-k",
|
|
380
|
+
"navigation"
|
|
381
|
+
]
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
"slug": "comments-threaded",
|
|
385
|
+
"title": "Threaded Comments",
|
|
386
|
+
"description": "Page + block-level threaded comments with resolved state. Real-time via Convex.",
|
|
387
|
+
"source": "notion-page-clone",
|
|
388
|
+
"files": [
|
|
389
|
+
"frontend/slices/notion/slices/comments/components/BlockCommentsPopover.tsx",
|
|
390
|
+
"frontend/slices/notion/slices/comments/hooks/useComments.ts"
|
|
391
|
+
],
|
|
392
|
+
"exampleCode": "import { BlockCommentsPopover } from \"@/frontend/slices/notion/slices/comments/components/BlockCommentsPopover\";\n\n<BlockCommentsPopover blockId={blockId} pageId={pageId} />",
|
|
393
|
+
"agentRecipe": "Anchor comments by passing pageId (always) and optional blockId. Use useComments(blockId) hook for the reactive list.",
|
|
394
|
+
"tags": [
|
|
395
|
+
"comments",
|
|
396
|
+
"real-time",
|
|
397
|
+
"threading"
|
|
398
|
+
]
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
"slug": "theme-preset-switcher",
|
|
402
|
+
"title": "Theme Preset Switcher",
|
|
403
|
+
"description": "Runtime theme swap (colors + fonts + shadows + tracking). OKLch CSS vars per preset. Persists to localStorage + Convex.",
|
|
404
|
+
"source": "rahmanef.com",
|
|
405
|
+
"files": [
|
|
406
|
+
"frontend/shared/theme/theme-presets.ts",
|
|
407
|
+
"frontend/shared/ui/components/theme-preset-switcher.tsx"
|
|
408
|
+
],
|
|
409
|
+
"exampleCode": "import { ThemePresetSwitcher } from \"@/frontend/shared/ui/components/theme-preset-switcher\";\n\n<ThemePresetSwitcher />",
|
|
410
|
+
"agentRecipe": "Add a new preset by appending a CSS block in app/globals.css with [data-theme=\"<name>\"], then register in preset-groups.ts.",
|
|
411
|
+
"tags": [
|
|
412
|
+
"theme",
|
|
413
|
+
"presets",
|
|
414
|
+
"oklch",
|
|
415
|
+
"design-system"
|
|
416
|
+
]
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
"slug": "contact-form-resend",
|
|
420
|
+
"title": "Contact Form + Resend",
|
|
421
|
+
"description": "Contact form posting to Resend email API. Server Action + Zod input validation.",
|
|
422
|
+
"source": "cescadesigns",
|
|
423
|
+
"files": [
|
|
424
|
+
"recipes/contact-form-resend/src/page.tsx"
|
|
425
|
+
],
|
|
426
|
+
"exampleCode": "// app/api/contact/route.ts\nimport { Resend } from \"resend\";\nconst resend = new Resend(process.env.RESEND_API_KEY!);\n\nexport async function POST(req: Request) {\n const data = await req.formData();\n await resend.emails.send({\n from: \"form@yourdomain.com\",\n to: \"you@yourdomain.com\",\n subject: `From ${data.get(\"name\")}`,\n html: `<p>${data.get(\"message\")}</p>`,\n });\n return Response.json({ ok: true });\n}",
|
|
427
|
+
"agentRecipe": "Wire ContactForm.tsx (form action /api/contact) to the route handler. Always validate inputs with Zod server-side.",
|
|
428
|
+
"tags": [
|
|
429
|
+
"form",
|
|
430
|
+
"email",
|
|
431
|
+
"resend",
|
|
432
|
+
"server-action"
|
|
433
|
+
]
|
|
434
|
+
}
|
|
435
|
+
],
|
|
436
|
+
"features": [
|
|
437
|
+
{
|
|
438
|
+
"slug": "ai-sdk-openrouter",
|
|
439
|
+
"title": "AI SDK — OpenRouter Router",
|
|
440
|
+
"category": "ai",
|
|
441
|
+
"description": "Tier-routed LLM calls via OpenRouter. Nano (Haiku/4o-mini) for classification, mid (Sonnet/4o) for drafting, flagship (Opus) for deep reasoning. Cost log + retry baked in.",
|
|
442
|
+
"source": "@openrouter/ai-sdk-provider + ai",
|
|
443
|
+
"docsUrl": "https://sdk.vercel.ai/docs",
|
|
444
|
+
"install": "npm i ai @openrouter/ai-sdk-provider",
|
|
445
|
+
"npmPackages": [
|
|
446
|
+
"ai",
|
|
447
|
+
"@openrouter/ai-sdk-provider"
|
|
448
|
+
],
|
|
449
|
+
"exampleCode": "// convex/shared/ai/router.ts\nimport { action } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { generateText } from \"ai\";\nimport { createOpenRouter } from \"@openrouter/ai-sdk-provider\";\n\nconst router = createOpenRouter({ apiKey: process.env.OPENROUTER_API_KEY! });\n\nconst TIER_TO_MODEL = {\n nano: \"anthropic/claude-haiku-4-5\",\n mid: \"anthropic/claude-sonnet-4-6\",\n flagship: \"anthropic/claude-opus-4-7\",\n};\n\nexport const callModel = action({\n args: {\n feature: v.string(),\n prompt: v.string(),\n tier: v.union(v.literal(\"nano\"), v.literal(\"mid\"), v.literal(\"flagship\")),\n },\n handler: async (ctx, { feature, prompt, tier }) => {\n const { text, usage } = await generateText({\n model: router(TIER_TO_MODEL[tier]),\n prompt,\n });\n await ctx.runMutation(internal.ai.logUsage, { feature, tier, usage });\n return text;\n },\n});",
|
|
450
|
+
"agentRecipe": "Wrap every AI call through ai-router action. Pick tier based on workload: nano for spam-flag/headline-suggest, mid for chat/draft, flagship for methodology-review. Log token usage to ai_usage table for cost dashboard.",
|
|
451
|
+
"tags": [
|
|
452
|
+
"ai",
|
|
453
|
+
"llm",
|
|
454
|
+
"openrouter",
|
|
455
|
+
"vercel-ai-sdk"
|
|
456
|
+
]
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
"slug": "convex-auth",
|
|
460
|
+
"title": "Convex Auth — Email Magic Link",
|
|
461
|
+
"category": "auth",
|
|
462
|
+
"description": "@convex-dev/auth with email magic link only. No Clerk, no NextAuth. Self-hosted Convex friendly. Hard mandate per kitab CLAUDE.md.",
|
|
463
|
+
"source": "@convex-dev/auth",
|
|
464
|
+
"docsUrl": "https://labs.convex.dev/auth",
|
|
465
|
+
"install": "npm i @convex-dev/auth @auth/core resend",
|
|
466
|
+
"npmPackages": [
|
|
467
|
+
"@convex-dev/auth",
|
|
468
|
+
"@auth/core",
|
|
469
|
+
"resend"
|
|
470
|
+
],
|
|
471
|
+
"exampleCode": "// convex/auth.ts\nimport { convexAuth } from \"@convex-dev/auth/server\";\nimport { ResendOTP } from \"./auth/ResendOTP\";\n\nexport const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({\n providers: [ResendOTP],\n});\n\n// app/proxy.ts (Next 16 — NOT middleware.ts)\nimport { convexAuthNextjsMiddleware } from \"@convex-dev/auth/nextjs/server\";\nexport default convexAuthNextjsMiddleware();",
|
|
472
|
+
"agentRecipe": "Mount auth in convex/auth.ts. Wire ResendOTP for magic-link delivery. Use convexAuthNextjsMiddleware in app/proxy.ts (Next 16 renamed middleware.ts → proxy.ts). Forbid Clerk per CLAUDE.md.",
|
|
473
|
+
"tags": [
|
|
474
|
+
"auth",
|
|
475
|
+
"convex",
|
|
476
|
+
"email-magic-link",
|
|
477
|
+
"no-clerk"
|
|
478
|
+
]
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
"slug": "broadcast-channel-sync",
|
|
482
|
+
"title": "BroadcastChannel — Cross-iframe Live Sync",
|
|
483
|
+
"category": "realtime",
|
|
484
|
+
"description": "Same-origin iframe live sync without backend. Used in T1 split preview tab — submit form di Public, action propagates ke Admin secara realtime via window.BroadcastChannel.",
|
|
485
|
+
"source": "Web Platform — BroadcastChannel API",
|
|
486
|
+
"docsUrl": "https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API",
|
|
487
|
+
"install": "// no install — Web Platform API",
|
|
488
|
+
"npmPackages": [],
|
|
489
|
+
"exampleCode": "\"use client\";\nimport * as React from \"react\";\n\nexport function StoreProvider({ children }) {\n const [state, baseDispatch] = React.useReducer(reducer, SEED_STATE);\n const channelRef = React.useRef<BroadcastChannel | null>(null);\n\n React.useEffect(() => {\n const ch = new BroadcastChannel(\"pbos:sync\");\n channelRef.current = ch;\n ch.onmessage = (e) => baseDispatch(e.data);\n return () => ch.close();\n }, []);\n\n const dispatch = React.useCallback((action) => {\n baseDispatch(action);\n channelRef.current?.postMessage(action);\n }, []);\n\n return <Ctx.Provider value={{ state, dispatch }}>{children}</Ctx.Provider>;\n}",
|
|
490
|
+
"agentRecipe": "Use BroadcastChannel only for demo / cross-iframe state mirroring. Production data still goes through Convex realtime. The channel does not echo to the sender so no loop.",
|
|
491
|
+
"tags": [
|
|
492
|
+
"realtime",
|
|
493
|
+
"broadcast-channel",
|
|
494
|
+
"cross-iframe",
|
|
495
|
+
"demo-pattern"
|
|
496
|
+
]
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
"slug": "convex-vector-search",
|
|
500
|
+
"title": "Convex Vector Index — Semantic Search",
|
|
501
|
+
"category": "search",
|
|
502
|
+
"description": "Built-in vector index on any Convex table. Embed via OpenAI text-embedding-3-small (1536-dim), query via vectorIndex().",
|
|
503
|
+
"source": "convex (built-in)",
|
|
504
|
+
"docsUrl": "https://docs.convex.dev/database/vector-search",
|
|
505
|
+
"install": "npm i openai",
|
|
506
|
+
"npmPackages": [
|
|
507
|
+
"openai"
|
|
508
|
+
],
|
|
509
|
+
"exampleCode": "// convex/schema.ts\nposts: defineTable({\n title: v.string(),\n body: v.string(),\n embedding: v.array(v.float64()),\n}).vectorIndex(\"by_embedding\", {\n vectorField: \"embedding\",\n dimensions: 1536,\n filterFields: [\"workspaceId\", \"status\"],\n}),\n\n// convex/posts.ts\nexport const search = action({\n args: { query: v.string(), workspaceId: v.id(\"workspaces\") },\n handler: async (ctx, args) => {\n const emb = await embed(args.query);\n return await ctx.vectorSearch(\"posts\", \"by_embedding\", {\n vector: emb,\n limit: 10,\n filter: (q) => q.eq(\"workspaceId\", args.workspaceId),\n });\n },\n});",
|
|
510
|
+
"agentRecipe": "Add embedding field + vectorIndex per searchable table. Re-embed on upsert via Convex action. Cache embeddings — don't re-call OpenAI on every read.",
|
|
511
|
+
"tags": [
|
|
512
|
+
"search",
|
|
513
|
+
"vector",
|
|
514
|
+
"convex",
|
|
515
|
+
"rag"
|
|
516
|
+
]
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
"slug": "resend-newsletter",
|
|
520
|
+
"title": "Resend — Transactional & Newsletter",
|
|
521
|
+
"category": "email",
|
|
522
|
+
"description": "Transactional email + newsletter blast via Resend. Double opt-in flow + audience segmentation. Magic-link delivery for Convex Auth.",
|
|
523
|
+
"source": "resend + react-email",
|
|
524
|
+
"docsUrl": "https://resend.com/docs",
|
|
525
|
+
"install": "npm i resend react-email @react-email/components",
|
|
526
|
+
"npmPackages": [
|
|
527
|
+
"resend",
|
|
528
|
+
"react-email",
|
|
529
|
+
"@react-email/components"
|
|
530
|
+
],
|
|
531
|
+
"exampleCode": "// convex/shared/email/resend.ts\nimport { Resend } from \"resend\";\nimport { action } from \"../../_generated/server\";\n\nconst resend = new Resend(process.env.RESEND_API_KEY!);\n\nexport const sendNewsletter = action({\n args: { audienceId: v.string(), subject: v.string(), html: v.string() },\n handler: async (_, args) => {\n await resend.broadcasts.create({\n audienceId: args.audienceId,\n from: \"lorem.dev <hi@lorem.dev>\",\n subject: args.subject,\n html: args.html,\n });\n },\n});",
|
|
532
|
+
"agentRecipe": "Use Resend Audiences API for newsletter — store subscriber emails in Convex too for segmentation. Double opt-in: subscriber.create with status 'pending' → click link → status 'confirmed'.",
|
|
533
|
+
"tags": [
|
|
534
|
+
"email",
|
|
535
|
+
"resend",
|
|
536
|
+
"newsletter",
|
|
537
|
+
"transactional"
|
|
538
|
+
]
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
"slug": "midtrans-payment",
|
|
542
|
+
"title": "Midtrans — Indonesia Payment",
|
|
543
|
+
"category": "payment",
|
|
544
|
+
"description": "Pembayaran lokal Indonesia via Midtrans Snap (BCA, Mandiri, BRI, e-wallet GoPay/OVO/Dana, QRIS). Webhook untuk konfirmasi.",
|
|
545
|
+
"source": "midtrans-client",
|
|
546
|
+
"docsUrl": "https://docs.midtrans.com",
|
|
547
|
+
"install": "npm i midtrans-client",
|
|
548
|
+
"npmPackages": [
|
|
549
|
+
"midtrans-client"
|
|
550
|
+
],
|
|
551
|
+
"exampleCode": "// convex/shared/billing/midtrans.ts\nimport midtransClient from \"midtrans-client\";\nimport { action } from \"../../_generated/server\";\n\nconst snap = new midtransClient.Snap({\n isProduction: false,\n serverKey: process.env.MIDTRANS_SERVER_KEY!,\n});\n\nexport const createPayment = action({\n args: { orderId: v.string(), amount: v.number(), customer: v.any() },\n handler: async (_, args) => {\n const tx = await snap.createTransaction({\n transaction_details: { order_id: args.orderId, gross_amount: args.amount },\n customer_details: args.customer,\n });\n return tx.redirect_url;\n },\n});",
|
|
552
|
+
"agentRecipe": "Midtrans Snap untuk pembayaran instant. Webhook ke Convex HTTP action /api/midtrans-callback untuk update order status. Ingat: PPN 11% sudah included di amount, jangan double-count.",
|
|
553
|
+
"tags": [
|
|
554
|
+
"payment",
|
|
555
|
+
"midtrans",
|
|
556
|
+
"indonesia",
|
|
557
|
+
"qris"
|
|
558
|
+
]
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
"slug": "mdx-blog",
|
|
562
|
+
"title": "MDX — Blog Content",
|
|
563
|
+
"category": "content",
|
|
564
|
+
"description": "Markdown-with-JSX untuk blog post. Auto-generate ToC, reading-time, syntax highlight, plus embed React components inline.",
|
|
565
|
+
"source": "next-mdx-remote",
|
|
566
|
+
"docsUrl": "https://github.com/hashicorp/next-mdx-remote",
|
|
567
|
+
"install": "npm i next-mdx-remote rehype-pretty-code remark-gfm reading-time",
|
|
568
|
+
"npmPackages": [
|
|
569
|
+
"next-mdx-remote",
|
|
570
|
+
"rehype-pretty-code",
|
|
571
|
+
"remark-gfm",
|
|
572
|
+
"reading-time"
|
|
573
|
+
],
|
|
574
|
+
"exampleCode": "// app/blog/[slug]/page.tsx\nimport { MDXRemote } from \"next-mdx-remote/rsc\";\nimport readingTime from \"reading-time\";\n\nexport default async function Page({ params }) {\n const { slug } = await params;\n const post = await getPost(slug);\n const stats = readingTime(post.body);\n\n return (\n <article>\n <h1>{post.title}</h1>\n <p>{stats.text}</p>\n <MDXRemote\n source={post.body}\n options={{ mdxOptions: { rehypePlugins: [rehypePrettyCode], remarkPlugins: [remarkGfm] } }}\n />\n </article>\n );\n}",
|
|
575
|
+
"agentRecipe": "Store post body sebagai markdown di Convex. Render dengan MDXRemote di [slug]/page.tsx. Auto-extract headings ke ToC via remark plugin custom.",
|
|
576
|
+
"tags": [
|
|
577
|
+
"mdx",
|
|
578
|
+
"markdown",
|
|
579
|
+
"blog",
|
|
580
|
+
"content"
|
|
581
|
+
]
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
"slug": "cal-com-booking",
|
|
585
|
+
"title": "Cal.com — Booking Embed",
|
|
586
|
+
"category": "data",
|
|
587
|
+
"description": "Embed Cal.com booking widget di halaman Services. Self-hosted atau cloud. Webhook ke Convex untuk sync booking ke leads table.",
|
|
588
|
+
"source": "@calcom/embed-react",
|
|
589
|
+
"docsUrl": "https://cal.com/docs/integrations/web-app/embed",
|
|
590
|
+
"install": "npm i @calcom/embed-react",
|
|
591
|
+
"npmPackages": [
|
|
592
|
+
"@calcom/embed-react"
|
|
593
|
+
],
|
|
594
|
+
"exampleCode": "\"use client\";\nimport Cal from \"@calcom/embed-react\";\n\nexport function CalEmbed({ eventType }: { eventType: string }) {\n return (\n <Cal\n calLink={`lorem/${eventType}`}\n style={{ width: \"100%\", height: \"600px\", overflow: \"scroll\" }}\n config={{ layout: \"month_view\", theme: \"dark\" }}\n />\n );\n}",
|
|
595
|
+
"agentRecipe": "Embed Cal.com via @calcom/embed-react di halaman services. Configure webhook di Cal.com dashboard → POST ke /api/cal-webhook → create lead di Convex.",
|
|
596
|
+
"tags": [
|
|
597
|
+
"booking",
|
|
598
|
+
"cal-com",
|
|
599
|
+
"scheduling",
|
|
600
|
+
"embed"
|
|
232
601
|
]
|
|
233
602
|
}
|
|
234
603
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rahman-resources",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Template installer for Rahman Resources kitab — npx rahman-resources add <template> <target>",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"gen": "node scripts/gen-manifest.mjs",
|
|
27
|
-
"
|
|
27
|
+
"validate": "node scripts/validate.mjs",
|
|
28
|
+
"prepublishOnly": "node scripts/validate.mjs"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
30
31
|
"kleur": "^4.1.5",
|