@cloudgrid-io/mcp 0.11.2 → 0.13.0
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 +2 -1
- package/src/corpus/capability-map.md +40 -6
- package/src/corpus/examples/ai-app/index.md +237 -0
- package/src/corpus/examples/api-service/index.md +190 -0
- package/src/corpus/examples/crm/index.md +273 -0
- package/src/corpus/examples/kanban/index.md +256 -0
- package/src/corpus/templates/admin-dashboard/README.md +75 -0
- package/src/corpus/templates/admin-dashboard/cloudgrid.yaml +15 -0
- package/src/corpus/templates/admin-dashboard/index.md +78 -0
- package/src/corpus/templates/admin-dashboard/services/web/app/api/orders/route.js +64 -0
- package/src/corpus/templates/admin-dashboard/services/web/app/api/users/route.js +63 -0
- package/src/corpus/templates/admin-dashboard/services/web/app/layout.js +39 -0
- package/src/corpus/templates/admin-dashboard/services/web/app/page.js +73 -0
- package/src/corpus/templates/admin-dashboard/services/web/app/resource-table.js +94 -0
- package/src/corpus/templates/admin-dashboard/services/web/lib/db.js +32 -0
- package/src/corpus/templates/admin-dashboard/services/web/package.json +16 -0
- package/src/corpus/templates/ai-app/README.md +95 -0
- package/src/corpus/templates/ai-app/cloudgrid.yaml +20 -0
- package/src/corpus/templates/ai-app/index.md +269 -0
- package/src/corpus/templates/ai-app/services/web/app/api/chat/route.js +51 -0
- package/src/corpus/templates/ai-app/services/web/app/chat.js +60 -0
- package/src/corpus/templates/ai-app/services/web/app/layout.js +36 -0
- package/src/corpus/templates/ai-app/services/web/app/page.js +25 -0
- package/src/corpus/templates/ai-app/services/web/lib/db.js +32 -0
- package/src/corpus/templates/ai-app/services/web/package.json +17 -0
- package/src/corpus/templates/api-docs/README.md +28 -0
- package/src/corpus/templates/api-docs/cloudgrid.yaml +10 -0
- package/src/corpus/templates/api-docs/index.html +136 -0
- package/src/corpus/templates/api-docs/index.md +30 -0
- package/src/corpus/templates/api-service/README.md +87 -0
- package/src/corpus/templates/api-service/cloudgrid.yaml +18 -0
- package/src/corpus/templates/api-service/index.md +225 -0
- package/src/corpus/templates/api-service/services/api/package.json +13 -0
- package/src/corpus/templates/api-service/services/api/src/index.js +132 -0
- package/src/corpus/templates/changelog/README.md +27 -0
- package/src/corpus/templates/changelog/cloudgrid.yaml +10 -0
- package/src/corpus/templates/changelog/index.html +80 -0
- package/src/corpus/templates/changelog/index.md +30 -0
- package/src/corpus/templates/crm/README.md +72 -0
- package/src/corpus/templates/crm/cloudgrid.yaml +15 -0
- package/src/corpus/templates/crm/index.md +68 -0
- package/src/corpus/templates/crm/services/web/app/api/contacts/route.js +80 -0
- package/src/corpus/templates/crm/services/web/app/contact-manager.js +102 -0
- package/src/corpus/templates/crm/services/web/app/layout.js +35 -0
- package/src/corpus/templates/crm/services/web/app/page.js +34 -0
- package/src/corpus/templates/crm/services/web/lib/db.js +32 -0
- package/src/corpus/templates/crm/services/web/package.json +16 -0
- package/src/corpus/templates/docs-site/README.md +28 -0
- package/src/corpus/templates/docs-site/cloudgrid.yaml +10 -0
- package/src/corpus/templates/docs-site/index.html +122 -0
- package/src/corpus/templates/docs-site/index.md +30 -0
- package/src/corpus/templates/kanban/README.md +72 -0
- package/src/corpus/templates/kanban/cloudgrid.yaml +15 -0
- package/src/corpus/templates/kanban/index.md +68 -0
- package/src/corpus/templates/kanban/services/web/app/api/cards/route.js +70 -0
- package/src/corpus/templates/kanban/services/web/app/board.js +93 -0
- package/src/corpus/templates/kanban/services/web/app/layout.js +37 -0
- package/src/corpus/templates/kanban/services/web/app/page.js +32 -0
- package/src/corpus/templates/kanban/services/web/lib/db.js +32 -0
- package/src/corpus/templates/kanban/services/web/package.json +16 -0
- package/src/corpus/templates/portfolio/README.md +29 -0
- package/src/corpus/templates/portfolio/cloudgrid.yaml +10 -0
- package/src/corpus/templates/portfolio/index.html +126 -0
- package/src/corpus/templates/portfolio/index.md +30 -0
- package/src/corpus/templates/saas-marketing/README.md +30 -0
- package/src/corpus/templates/saas-marketing/cloudgrid.yaml +10 -0
- package/src/corpus/templates/saas-marketing/index.html +212 -0
- package/src/corpus/templates/saas-marketing/index.md +30 -0
- package/src/corpus/templates/status-page/README.md +28 -0
- package/src/corpus/templates/status-page/cloudgrid.yaml +10 -0
- package/src/corpus/templates/status-page/index.html +117 -0
- package/src/corpus/templates/status-page/index.md +30 -0
- package/src/corpus/templates/task-manager/README.md +72 -0
- package/src/corpus/templates/task-manager/cloudgrid.yaml +15 -0
- package/src/corpus/templates/task-manager/index.md +68 -0
- package/src/corpus/templates/task-manager/services/web/app/api/tasks/route.js +77 -0
- package/src/corpus/templates/task-manager/services/web/app/layout.js +39 -0
- package/src/corpus/templates/task-manager/services/web/app/page.js +33 -0
- package/src/corpus/templates/task-manager/services/web/app/task-list.js +110 -0
- package/src/corpus/templates/task-manager/services/web/lib/db.js +32 -0
- package/src/corpus/templates/task-manager/services/web/package.json +16 -0
- package/src/corpus/templates/waitlist/README.md +38 -0
- package/src/corpus/templates/waitlist/cloudgrid.yaml +10 -0
- package/src/corpus/templates/waitlist/index.html +69 -0
- package/src/corpus/templates/waitlist/index.md +41 -0
- package/src/corpus/workflows/admin-dashboard.md +102 -0
- package/src/corpus/workflows/ai-app.md +136 -0
- package/src/corpus/workflows/api-docs.md +36 -0
- package/src/corpus/workflows/api-service.md +118 -0
- package/src/corpus/workflows/changelog.md +33 -0
- package/src/corpus/workflows/crm.md +101 -0
- package/src/corpus/workflows/docs-site.md +35 -0
- package/src/corpus/workflows/kanban.md +102 -0
- package/src/corpus/workflows/portfolio.md +35 -0
- package/src/corpus/workflows/saas-marketing.md +37 -0
- package/src/corpus/workflows/status-page.md +35 -0
- package/src/corpus/workflows/task-manager.md +101 -0
- package/src/corpus/workflows/waitlist.md +42 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# kanban template — persistent Next.js + Mongo board
|
|
2
|
+
|
|
3
|
+
A real, deployable kanban board. Cards live in the grid-shared MongoDB, so they
|
|
4
|
+
survive refresh and are shared across users/sessions. Same proven shape as
|
|
5
|
+
`app-with-data`, adapted to a `cards` domain (title, column, order) with three
|
|
6
|
+
columns and move-between-columns.
|
|
7
|
+
|
|
8
|
+
## How the grid gives you a database
|
|
9
|
+
|
|
10
|
+
You do **not** provision a database or set a connection string. In
|
|
11
|
+
`cloudgrid.yaml` you declare `needs: { database: true }`, and the grid:
|
|
12
|
+
|
|
13
|
+
- provisions shared Mongo for the app, and
|
|
14
|
+
- injects the connection string as the **`DATABASE_MONGODB_URL`** environment
|
|
15
|
+
variable (plus the legacy `MONGODB_URL` alias) — at dev-time (`grid dev`) and
|
|
16
|
+
at runtime (after `grid plug`).
|
|
17
|
+
|
|
18
|
+
The app reads it via `process.env.DATABASE_MONGODB_URL` (with a legacy
|
|
19
|
+
`process.env.MONGODB_URL` fallback) in `services/web/lib/db.js` — **lazily,
|
|
20
|
+
inside the `getDb` getter, never at module top level** (a top-level read fails
|
|
21
|
+
`next build`, which imports the module for route analysis before the grid injects
|
|
22
|
+
the var). Never hardcode a connection string; never commit a secret.
|
|
23
|
+
|
|
24
|
+
> **Declare `needs: { database: true }` (the canonical shape).** The deployer
|
|
25
|
+
> provisions Mongo and injects `DATABASE_MONGODB_URL` (plus the legacy
|
|
26
|
+
> `MONGODB_URL` alias). `requires:` is the deprecated v1 alias — don't author new
|
|
27
|
+
> yaml with it, and never set `needs:` and `requires:` together (the validator
|
|
28
|
+
> rejects the combination).
|
|
29
|
+
|
|
30
|
+
## Service layout
|
|
31
|
+
|
|
32
|
+
App code lives under **`services/web/`**, not the template root. `path:` in
|
|
33
|
+
`cloudgrid.yaml` is the URL mount, not the filesystem path — the service named
|
|
34
|
+
`web` means the CLI looks for `services/web/`. Files at the root fail with
|
|
35
|
+
`Error: Service directory not found: …/services/web`.
|
|
36
|
+
|
|
37
|
+
## Run locally
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install
|
|
41
|
+
grid dev # runs Next.js with DATABASE_MONGODB_URL injected against dev Mongo
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Deploy
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
grid plug # builds + deploys the folder (async — poll status until live)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A runtime deploy is asynchronous: `plug` returns `status: building`; poll status
|
|
51
|
+
until the app is live, then use the returned live URL. Re-plug the same entity
|
|
52
|
+
to update the same URL.
|
|
53
|
+
|
|
54
|
+
## File tree
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
cloudgrid.yaml # name + services.web (nextjs) + needs: { database: true }
|
|
58
|
+
services/web/package.json # next, react, react-dom, mongodb driver only
|
|
59
|
+
services/web/lib/db.js # lazy Mongo client from DATABASE_MONGODB_URL (legacy MONGODB_URL fallback)
|
|
60
|
+
services/web/app/layout.js # root layout + inline CSS
|
|
61
|
+
services/web/app/page.js # server component: reads cards from Mongo
|
|
62
|
+
services/web/app/board.js # client board: POST / PATCH (move column) / DELETE via the API
|
|
63
|
+
services/web/app/api/cards/route.js # GET (list) / POST (add) / PATCH (move) / DELETE (remove)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Adapt it
|
|
67
|
+
|
|
68
|
+
- Rename the `cards` collection in `services/web/app/api/cards/route.js` and
|
|
69
|
+
`services/web/app/page.js`.
|
|
70
|
+
- Change the columns (edit the `COLUMNS` array) or add fields (assignee, labels).
|
|
71
|
+
- Add drag-and-drop or per-column ordering on top of the `order` field.
|
|
72
|
+
- Add `cache: true` to `needs:` only if you actually need Redis.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Rename this app. The grid injects the DB connection string as an environment
|
|
2
|
+
# variable at runtime — do NOT set it yourself, and never commit a connection
|
|
3
|
+
# string or secret.
|
|
4
|
+
#
|
|
5
|
+
# `needs: { database: true }` is the canonical, recommended shape. The deployer
|
|
6
|
+
# provisions Mongo and injects DATABASE_MONGODB_URL (plus the legacy MONGODB_URL
|
|
7
|
+
# alias). `requires:` is the deprecated v1 alias — don't author new yaml with it,
|
|
8
|
+
# and never set `needs:` and `requires:` together (the validator rejects it).
|
|
9
|
+
name: my-board
|
|
10
|
+
services:
|
|
11
|
+
web:
|
|
12
|
+
type: nextjs
|
|
13
|
+
path: /
|
|
14
|
+
needs:
|
|
15
|
+
database: true
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Template: kanban (persistent Next.js + Mongo board)
|
|
2
|
+
|
|
3
|
+
A real, deployable kanban board. Cards live in the grid-shared MongoDB, so they
|
|
4
|
+
survive refresh and are shared across sessions. Same proven shape as
|
|
5
|
+
`app-with-data`, adapted to a `cards` domain (title, column, order) with three
|
|
6
|
+
columns and move-between-columns.
|
|
7
|
+
|
|
8
|
+
**Key rules (all proven by the app-with-data end-to-end deploy):**
|
|
9
|
+
|
|
10
|
+
1. **Service code MUST live under `services/<name>/`, not the repo/template
|
|
11
|
+
root.** `path:` in `cloudgrid.yaml` is the URL mount, NOT the filesystem path.
|
|
12
|
+
The service named `web` → the CLI looks for `services/web/`. App files at the
|
|
13
|
+
root fail with `Error: Service directory not found: …/services/web`.
|
|
14
|
+
2. **Read the DB connection string LAZILY (inside the getter), never at module
|
|
15
|
+
top level.** The grid injects it as `DATABASE_MONGODB_URL` (plus the legacy
|
|
16
|
+
`MONGODB_URL` alias) at dev-time and runtime; the app reads
|
|
17
|
+
`process.env.DATABASE_MONGODB_URL || process.env.MONGODB_URL` inside `getDb`.
|
|
18
|
+
A top-level read fails `next build`. Never hardcode a connection string; never
|
|
19
|
+
commit a secret.
|
|
20
|
+
3. **Declare the datastore with `needs: { database: true }`.** The deployer
|
|
21
|
+
provisions Mongo and injects the connection string. `requires:` is the
|
|
22
|
+
deprecated v1 alias; don't author new yaml with it, and never set `needs:` and
|
|
23
|
+
`requires:` together (the validator rejects the combination).
|
|
24
|
+
|
|
25
|
+
## File tree
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
cloudgrid.yaml # name + services.web (nextjs) + needs: { database: true }
|
|
29
|
+
services/web/package.json # next, react, react-dom, mongodb driver only
|
|
30
|
+
services/web/lib/db.js # lazy Mongo client from DATABASE_MONGODB_URL (legacy MONGODB_URL fallback)
|
|
31
|
+
services/web/app/layout.js # root layout + inline CSS
|
|
32
|
+
services/web/app/page.js # server component: reads cards from Mongo
|
|
33
|
+
services/web/app/board.js # client board: POST / PATCH (move column) / DELETE via the API
|
|
34
|
+
services/web/app/api/cards/route.js # GET (list) / POST (add) / PATCH (move) / DELETE (remove)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## cloudgrid.yaml
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
name: my-board
|
|
41
|
+
services:
|
|
42
|
+
web:
|
|
43
|
+
type: nextjs
|
|
44
|
+
path: /
|
|
45
|
+
needs:
|
|
46
|
+
database: true
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> **Capability:** this template's need is `database: true`. The deployer
|
|
50
|
+
> provisions Mongo and injects `DATABASE_MONGODB_URL` (plus the legacy
|
|
51
|
+
> `MONGODB_URL` alias). `requires:` is the deprecated v1 alias — don't mix it with
|
|
52
|
+
> `needs:` (the validator rejects the combination). See the capability-map for
|
|
53
|
+
> the full injection table.
|
|
54
|
+
|
|
55
|
+
## The cards collection
|
|
56
|
+
|
|
57
|
+
Documents: `{ title, column, order, createdAt }` where `column` is one of `todo`
|
|
58
|
+
/ `doing` / `done`. The API route on `cards` does GET (list, ordered), POST
|
|
59
|
+
(add to `todo`), PATCH (move to another column), DELETE (remove); the page is a
|
|
60
|
+
server component that reads the collection and hands it to a client board.
|
|
61
|
+
|
|
62
|
+
## Adapt it
|
|
63
|
+
|
|
64
|
+
- Rename the `cards` collection to your data.
|
|
65
|
+
- Change the columns (edit the `COLUMNS` array) or add fields (assignee, labels).
|
|
66
|
+
- Add drag-and-drop or per-column ordering on top of the `order` field.
|
|
67
|
+
- Add `cache: true` to `needs:` only if you actually need Redis.
|
|
68
|
+
- Run `grid dev` to test locally, `grid plug` to deploy (async — poll to live).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// App Router route handler for the `cards` collection.
|
|
2
|
+
// GET -> list cards, POST -> add a card, PATCH -> move a card (change column),
|
|
3
|
+
// DELETE -> remove a card by id.
|
|
4
|
+
//
|
|
5
|
+
// To adapt this app, rename the collection, change the columns, or add fields.
|
|
6
|
+
import { NextResponse } from "next/server";
|
|
7
|
+
import { ObjectId } from "mongodb";
|
|
8
|
+
import { getDb } from "../../../lib/db.js";
|
|
9
|
+
|
|
10
|
+
// Never cache — always read/write live data.
|
|
11
|
+
export const dynamic = "force-dynamic";
|
|
12
|
+
|
|
13
|
+
const COLUMNS = ["todo", "doing", "done"];
|
|
14
|
+
|
|
15
|
+
async function cards() {
|
|
16
|
+
const db = await getDb();
|
|
17
|
+
return db.collection("cards");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function shape(c) {
|
|
21
|
+
return {
|
|
22
|
+
id: c._id.toString(),
|
|
23
|
+
title: c.title || "",
|
|
24
|
+
column: COLUMNS.includes(c.column) ? c.column : "todo",
|
|
25
|
+
order: typeof c.order === "number" ? c.order : 0,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// GET /api/cards — list all cards, ordered within a column.
|
|
30
|
+
export async function GET() {
|
|
31
|
+
const col = await cards();
|
|
32
|
+
const items = await col.find({}).sort({ order: 1, createdAt: 1 }).toArray();
|
|
33
|
+
return NextResponse.json(items.map(shape));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// POST /api/cards — add a card. Body: { title }.
|
|
37
|
+
export async function POST(request) {
|
|
38
|
+
const body = await request.json().catch(() => ({}));
|
|
39
|
+
const title = typeof body.title === "string" ? body.title.trim() : "";
|
|
40
|
+
if (!title) {
|
|
41
|
+
return NextResponse.json({ error: "title is required" }, { status: 400 });
|
|
42
|
+
}
|
|
43
|
+
const doc = { title, column: "todo", order: Date.now(), createdAt: new Date() };
|
|
44
|
+
const col = await cards();
|
|
45
|
+
const res = await col.insertOne(doc);
|
|
46
|
+
return NextResponse.json({ id: res.insertedId.toString(), ...doc }, { status: 201 });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// PATCH /api/cards — move a card to another column. Body: { id, column }.
|
|
50
|
+
export async function PATCH(request) {
|
|
51
|
+
const body = await request.json().catch(() => ({}));
|
|
52
|
+
const { id, column } = body;
|
|
53
|
+
if (!id || !ObjectId.isValid(id) || !COLUMNS.includes(column)) {
|
|
54
|
+
return NextResponse.json({ error: "valid id and column required" }, { status: 400 });
|
|
55
|
+
}
|
|
56
|
+
const col = await cards();
|
|
57
|
+
await col.updateOne({ _id: new ObjectId(id) }, { $set: { column } });
|
|
58
|
+
return NextResponse.json({ ok: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// DELETE /api/cards?id=<id> — remove a card.
|
|
62
|
+
export async function DELETE(request) {
|
|
63
|
+
const id = new URL(request.url).searchParams.get("id");
|
|
64
|
+
if (!id || !ObjectId.isValid(id)) {
|
|
65
|
+
return NextResponse.json({ error: "valid id is required" }, { status: 400 });
|
|
66
|
+
}
|
|
67
|
+
const col = await cards();
|
|
68
|
+
await col.deleteOne({ _id: new ObjectId(id) });
|
|
69
|
+
return NextResponse.json({ ok: true });
|
|
70
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Small client component: renders the three columns and posts to /api/cards.
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
|
|
6
|
+
const COLUMNS = ["todo", "doing", "done"];
|
|
7
|
+
const LABELS = { todo: "To do", doing: "Doing", done: "Done" };
|
|
8
|
+
|
|
9
|
+
export default function Board({ initialCards }) {
|
|
10
|
+
const [cards, setCards] = useState(initialCards);
|
|
11
|
+
const [title, setTitle] = useState("");
|
|
12
|
+
const [busy, setBusy] = useState(false);
|
|
13
|
+
|
|
14
|
+
async function add(e) {
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
const value = title.trim();
|
|
17
|
+
if (!value) return;
|
|
18
|
+
setBusy(true);
|
|
19
|
+
const res = await fetch("/api/cards", {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: { "content-type": "application/json" },
|
|
22
|
+
body: JSON.stringify({ title: value }),
|
|
23
|
+
});
|
|
24
|
+
if (res.ok) {
|
|
25
|
+
const card = await res.json();
|
|
26
|
+
setCards((prev) => [...prev, card]);
|
|
27
|
+
setTitle("");
|
|
28
|
+
}
|
|
29
|
+
setBusy(false);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function move(id, column) {
|
|
33
|
+
setBusy(true);
|
|
34
|
+
const res = await fetch("/api/cards", {
|
|
35
|
+
method: "PATCH",
|
|
36
|
+
headers: { "content-type": "application/json" },
|
|
37
|
+
body: JSON.stringify({ id, column }),
|
|
38
|
+
});
|
|
39
|
+
if (res.ok) setCards((prev) => prev.map((c) => (c.id === id ? { ...c, column } : c)));
|
|
40
|
+
setBusy(false);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function remove(id) {
|
|
44
|
+
setBusy(true);
|
|
45
|
+
const res = await fetch(`/api/cards?id=${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
46
|
+
if (res.ok) setCards((prev) => prev.filter((c) => c.id !== id));
|
|
47
|
+
setBusy(false);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div>
|
|
52
|
+
<form onSubmit={add} className="row">
|
|
53
|
+
<input
|
|
54
|
+
value={title}
|
|
55
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
56
|
+
placeholder="Add a card…"
|
|
57
|
+
aria-label="New card"
|
|
58
|
+
/>
|
|
59
|
+
<button type="submit" disabled={busy}>Add</button>
|
|
60
|
+
</form>
|
|
61
|
+
<div className="board">
|
|
62
|
+
{COLUMNS.map((column) => (
|
|
63
|
+
<section key={column}>
|
|
64
|
+
<h2>{LABELS[column]}</h2>
|
|
65
|
+
{cards.filter((c) => c.column === column).length === 0 && (
|
|
66
|
+
<p className="empty">No cards.</p>
|
|
67
|
+
)}
|
|
68
|
+
{cards
|
|
69
|
+
.filter((c) => c.column === column)
|
|
70
|
+
.map((c) => (
|
|
71
|
+
<article key={c.id} className="card">
|
|
72
|
+
<span className="title">{c.title}</span>
|
|
73
|
+
<div className="moves">
|
|
74
|
+
{COLUMNS.filter((other) => other !== column).map((other) => (
|
|
75
|
+
<button
|
|
76
|
+
key={other}
|
|
77
|
+
type="button"
|
|
78
|
+
onClick={() => move(c.id, other)}
|
|
79
|
+
disabled={busy}
|
|
80
|
+
>
|
|
81
|
+
→ {LABELS[other]}
|
|
82
|
+
</button>
|
|
83
|
+
))}
|
|
84
|
+
<button type="button" onClick={() => remove(c.id)} disabled={busy}>Delete</button>
|
|
85
|
+
</div>
|
|
86
|
+
</article>
|
|
87
|
+
))}
|
|
88
|
+
</section>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Root layout + plain inline CSS (no external CDNs, fonts, or stylesheets).
|
|
2
|
+
export const metadata = {
|
|
3
|
+
title: "Kanban board",
|
|
4
|
+
description: "A persistent kanban board on CloudGrid, backed by grid-shared Mongo.",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const css = `
|
|
8
|
+
:root { color-scheme: light dark; }
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
body { margin: 0; font-family: system-ui, -apple-system, sans-serif; line-height: 1.5; }
|
|
11
|
+
main { max-width: 60rem; margin: 3rem auto; padding: 0 1.25rem; }
|
|
12
|
+
h1 { margin: 0 0 .25rem; }
|
|
13
|
+
.hint { margin: 0 0 1.5rem; opacity: .7; font-size: .9rem; }
|
|
14
|
+
.row { display: flex; gap: .5rem; margin-bottom: 1.5rem; }
|
|
15
|
+
input { flex: 1; padding: .5rem .75rem; border: 1px solid #8886; border-radius: .5rem; font: inherit; }
|
|
16
|
+
button { padding: .4rem .7rem; border: 1px solid #8886; border-radius: .5rem; background: #6663; font: inherit; cursor: pointer; }
|
|
17
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
18
|
+
.board { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
|
|
19
|
+
.board section { border: 1px solid #8883; border-radius: .5rem; padding: .75rem; min-height: 6rem; }
|
|
20
|
+
.board h2 { margin: 0 0 .5rem; font-size: .9rem; text-transform: uppercase; letter-spacing: .03em; opacity: .7; }
|
|
21
|
+
.card { border: 1px solid #8886; border-radius: .5rem; padding: .5rem .6rem; margin-bottom: .5rem; }
|
|
22
|
+
.card .title { display: block; margin-bottom: .4rem; }
|
|
23
|
+
.moves { display: flex; flex-wrap: wrap; gap: .3rem; }
|
|
24
|
+
.moves button { font-size: .8rem; }
|
|
25
|
+
.empty { opacity: .6; font-size: .85rem; }
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
export default function RootLayout({ children }) {
|
|
29
|
+
return (
|
|
30
|
+
<html lang="en">
|
|
31
|
+
<head>
|
|
32
|
+
<style dangerouslySetInnerHTML={{ __html: css }} />
|
|
33
|
+
</head>
|
|
34
|
+
<body>{children}</body>
|
|
35
|
+
</html>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Home page: a server component reads the cards straight from Mongo, and a small
|
|
2
|
+
// client board adds / moves / removes them via the API route. Data persists
|
|
3
|
+
// across refresh and across users because it lives in the grid-shared Mongo, not
|
|
4
|
+
// in memory.
|
|
5
|
+
import { getDb } from "../lib/db.js";
|
|
6
|
+
import Board from "./board.js";
|
|
7
|
+
|
|
8
|
+
export const dynamic = "force-dynamic";
|
|
9
|
+
|
|
10
|
+
const COLUMNS = ["todo", "doing", "done"];
|
|
11
|
+
|
|
12
|
+
async function listCards() {
|
|
13
|
+
const db = await getDb();
|
|
14
|
+
const items = await db.collection("cards").find({}).sort({ order: 1, createdAt: 1 }).toArray();
|
|
15
|
+
return items.map((c) => ({
|
|
16
|
+
id: c._id.toString(),
|
|
17
|
+
title: c.title || "",
|
|
18
|
+
column: COLUMNS.includes(c.column) ? c.column : "todo",
|
|
19
|
+
order: typeof c.order === "number" ? c.order : 0,
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default async function Page() {
|
|
24
|
+
const cards = await listCards();
|
|
25
|
+
return (
|
|
26
|
+
<main>
|
|
27
|
+
<h1>Board</h1>
|
|
28
|
+
<p className="hint">Persisted in the grid-shared Mongo — survives refresh.</p>
|
|
29
|
+
<Board initialCards={cards} />
|
|
30
|
+
</main>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Cached MongoDB client for the App Router.
|
|
2
|
+
//
|
|
3
|
+
// The grid injects the connection string as the DATABASE_MONGODB_URL environment
|
|
4
|
+
// variable (plus the legacy MONGODB_URL alias) at dev-time (`grid dev`) and
|
|
5
|
+
// runtime (after `grid plug`). Read the canonical var first, fall back to the
|
|
6
|
+
// legacy alias. Never hardcode a connection string here and never commit a secret.
|
|
7
|
+
//
|
|
8
|
+
// The env var and client are resolved LAZILY (inside getDb), never at module top
|
|
9
|
+
// level — otherwise `next build` throws when it imports this module for route
|
|
10
|
+
// analysis, before the grid injects the var.
|
|
11
|
+
import { MongoClient } from "mongodb";
|
|
12
|
+
|
|
13
|
+
function clientPromise() {
|
|
14
|
+
const uri = process.env.DATABASE_MONGODB_URL || process.env.MONGODB_URL;
|
|
15
|
+
if (!uri) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"DATABASE_MONGODB_URL is not set. The grid injects it automatically — run " +
|
|
18
|
+
"this app with `grid dev` locally, or deploy it with `grid plug` (the grid " +
|
|
19
|
+
"injects the DB connection string at runtime). Do not set it by hand.",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
if (!globalThis.__mongoClientPromise) {
|
|
23
|
+
globalThis.__mongoClientPromise = new MongoClient(uri).connect();
|
|
24
|
+
}
|
|
25
|
+
return globalThis.__mongoClientPromise;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function getDb() {
|
|
29
|
+
const client = await clientPromise();
|
|
30
|
+
// The default DB comes from the connection-string path segment the grid injects.
|
|
31
|
+
return client.db();
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-board",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"next": "^15.1.0",
|
|
12
|
+
"react": "^19.0.0",
|
|
13
|
+
"react-dom": "^19.0.0",
|
|
14
|
+
"mongodb": "^6.12.0"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# portfolio
|
|
2
|
+
|
|
3
|
+
A single self-contained HTML personal / freelancer portfolio: a hero intro, an about section, a skills list, a projects grid, and a contact section.
|
|
4
|
+
|
|
5
|
+
Static inspiration — a single self-contained `index.html` (inline CSS and JS,
|
|
6
|
+
no external CDNs). It deploys instantly via `gridctl_drop` and works on every
|
|
7
|
+
CloudGrid edition, including hosted.
|
|
8
|
+
|
|
9
|
+
## Fill it in
|
|
10
|
+
|
|
11
|
+
Fetch the template with `gridctl_fetch("template", "portfolio")`, then replace the
|
|
12
|
+
placeholders:
|
|
13
|
+
|
|
14
|
+
- The person's name, role/title, and short intro.
|
|
15
|
+
- The about paragraph.
|
|
16
|
+
- The skills — one pill per skill.
|
|
17
|
+
- One card per project: title, short description, tags, and an optional link.
|
|
18
|
+
- The contact links (email as a mailto, plus optional social/profile links).
|
|
19
|
+
|
|
20
|
+
Keep it a single self-contained HTML file. Do not add external scripts,
|
|
21
|
+
stylesheets, fonts, or large embedded media.
|
|
22
|
+
|
|
23
|
+
## Deploy
|
|
24
|
+
|
|
25
|
+
- Hosted MCP edition: call the drop tool with the HTML.
|
|
26
|
+
- Local MCP / CLI edition: write the HTML to a file and run `gridctl plug`.
|
|
27
|
+
|
|
28
|
+
It deploys as an inspiration and returns a live share URL right away. Re-deploy
|
|
29
|
+
with the same entity id to update the same URL.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# static — deploys as an inspiration (instant, works on hosted) via gridctl_drop;
|
|
2
|
+
# this yaml is only for plugging it as an OWNED static runtime (async, local
|
|
3
|
+
# edition only). To publish the page, prefer gridctl_drop, not grid plug.
|
|
4
|
+
#
|
|
5
|
+
# needs: {} # none — static, no infrastructure
|
|
6
|
+
name: portfolio
|
|
7
|
+
services:
|
|
8
|
+
web:
|
|
9
|
+
type: static
|
|
10
|
+
path: /
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>{{FULL_NAME}} — {{ROLE}}</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0b0f17; --panel: #121826; --fg: #e8edf5; --muted: #9aa7bd;
|
|
10
|
+
--accent: #5b8cff; --border: #223049;
|
|
11
|
+
}
|
|
12
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
13
|
+
html { scroll-behavior: smooth; }
|
|
14
|
+
body {
|
|
15
|
+
background: var(--bg); color: var(--fg);
|
|
16
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
17
|
+
-webkit-font-smoothing: antialiased; line-height: 1.6;
|
|
18
|
+
}
|
|
19
|
+
.wrap { max-width: 900px; margin: 0 auto; padding: 0 1.25rem; }
|
|
20
|
+
nav { display: flex; justify-content: space-between; align-items: center; padding: 1.25rem; max-width: 900px; margin: 0 auto; }
|
|
21
|
+
nav .brand { font-weight: 600; letter-spacing: -0.01em; }
|
|
22
|
+
nav .links { display: flex; gap: 1.25rem; }
|
|
23
|
+
nav .links a { color: var(--muted); text-decoration: none; font-size: 0.92rem; }
|
|
24
|
+
nav .links a:hover { color: var(--fg); }
|
|
25
|
+
|
|
26
|
+
section { padding: clamp(2.5rem, 7vw, 5rem) 0; }
|
|
27
|
+
.hero { text-align: left; }
|
|
28
|
+
.hero .eyebrow { color: var(--accent); font-weight: 600; font-size: 0.9rem; }
|
|
29
|
+
.hero h1 { font-size: clamp(2.4rem, 7vw, 4rem); letter-spacing: -0.02em; line-height: 1.05; margin: 0.5rem 0; }
|
|
30
|
+
.hero .role { color: var(--muted); font-size: 1.3rem; margin-bottom: 1rem; }
|
|
31
|
+
.hero p { color: var(--muted); max-width: 54ch; font-size: 1.1rem; }
|
|
32
|
+
|
|
33
|
+
h2 { font-size: clamp(1.5rem, 4vw, 2rem); letter-spacing: -0.01em; margin-bottom: 1.25rem; }
|
|
34
|
+
.about p { color: var(--muted); max-width: 60ch; }
|
|
35
|
+
|
|
36
|
+
.skills { display: flex; flex-wrap: wrap; gap: 0.6rem; }
|
|
37
|
+
.skill { background: var(--panel); border: 1px solid var(--border); border-radius: 999px; padding: 0.4rem 0.9rem; font-size: 0.9rem; }
|
|
38
|
+
|
|
39
|
+
.projects { display: grid; gap: 1.25rem; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
|
|
40
|
+
.project { background: var(--panel); border: 1px solid var(--border); border-radius: 14px; padding: 1.5rem; display: grid; gap: 0.6rem; align-content: start; }
|
|
41
|
+
.project h3 { font-size: 1.2rem; letter-spacing: -0.01em; }
|
|
42
|
+
.project p { color: var(--muted); }
|
|
43
|
+
.project .tags { display: flex; flex-wrap: wrap; gap: 0.4rem; }
|
|
44
|
+
.project .tags span { font-size: 0.75rem; color: var(--muted); border: 1px solid var(--border); border-radius: 6px; padding: 0.1rem 0.5rem; }
|
|
45
|
+
.project a { color: var(--accent); text-decoration: none; font-size: 0.9rem; font-weight: 600; }
|
|
46
|
+
|
|
47
|
+
.contact { text-align: center; border-top: 1px solid var(--border); }
|
|
48
|
+
.contact .actions { display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; margin-top: 1rem; }
|
|
49
|
+
.btn { display: inline-block; background: var(--accent); color: #fff; text-decoration: none; padding: 0.75rem 1.4rem; border-radius: 10px; font-weight: 600; }
|
|
50
|
+
.btn.ghost { background: transparent; border: 1px solid var(--border); color: var(--fg); }
|
|
51
|
+
footer { color: var(--muted); font-size: 0.85rem; text-align: center; padding-bottom: 2.5rem; }
|
|
52
|
+
</style>
|
|
53
|
+
</head>
|
|
54
|
+
<body>
|
|
55
|
+
<nav>
|
|
56
|
+
<span class="brand">{{FULL_NAME}}</span>
|
|
57
|
+
<div class="links">
|
|
58
|
+
<a href="#about">About</a>
|
|
59
|
+
<a href="#work">Work</a>
|
|
60
|
+
<a href="#contact">Contact</a>
|
|
61
|
+
</div>
|
|
62
|
+
</nav>
|
|
63
|
+
|
|
64
|
+
<div class="wrap">
|
|
65
|
+
<section class="hero">
|
|
66
|
+
<span class="eyebrow">{{HERO_EYEBROW}}</span>
|
|
67
|
+
<h1>{{FULL_NAME}}</h1>
|
|
68
|
+
<div class="role">{{ROLE}}</div>
|
|
69
|
+
<p>{{HERO_INTRO}}</p>
|
|
70
|
+
</section>
|
|
71
|
+
|
|
72
|
+
<section id="about" class="about">
|
|
73
|
+
<h2>About</h2>
|
|
74
|
+
<p>{{ABOUT_BODY}}</p>
|
|
75
|
+
<div class="skills" style="margin-top:1.25rem">
|
|
76
|
+
<!-- SKILLS_START -->
|
|
77
|
+
<span class="skill">{{SKILL_1}}</span>
|
|
78
|
+
<span class="skill">{{SKILL_2}}</span>
|
|
79
|
+
<span class="skill">{{SKILL_3}}</span>
|
|
80
|
+
<span class="skill">{{SKILL_4}}</span>
|
|
81
|
+
<span class="skill">{{SKILL_5}}</span>
|
|
82
|
+
<!-- Add more <span class="skill">...</span> as needed. -->
|
|
83
|
+
<!-- SKILLS_END -->
|
|
84
|
+
</div>
|
|
85
|
+
</section>
|
|
86
|
+
|
|
87
|
+
<section id="work">
|
|
88
|
+
<h2>Selected work</h2>
|
|
89
|
+
<div class="projects">
|
|
90
|
+
<!-- PROJECTS_START -->
|
|
91
|
+
<div class="project">
|
|
92
|
+
<h3>{{PROJECT_1_TITLE}}</h3>
|
|
93
|
+
<p>{{PROJECT_1_BODY}}</p>
|
|
94
|
+
<div class="tags"><span>{{PROJECT_1_TAG_1}}</span><span>{{PROJECT_1_TAG_2}}</span></div>
|
|
95
|
+
<a href="{{PROJECT_1_LINK}}">View project</a>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="project">
|
|
98
|
+
<h3>{{PROJECT_2_TITLE}}</h3>
|
|
99
|
+
<p>{{PROJECT_2_BODY}}</p>
|
|
100
|
+
<div class="tags"><span>{{PROJECT_2_TAG_1}}</span><span>{{PROJECT_2_TAG_2}}</span></div>
|
|
101
|
+
<a href="{{PROJECT_2_LINK}}">View project</a>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="project">
|
|
104
|
+
<h3>{{PROJECT_3_TITLE}}</h3>
|
|
105
|
+
<p>{{PROJECT_3_BODY}}</p>
|
|
106
|
+
<div class="tags"><span>{{PROJECT_3_TAG_1}}</span><span>{{PROJECT_3_TAG_2}}</span></div>
|
|
107
|
+
<a href="{{PROJECT_3_LINK}}">View project</a>
|
|
108
|
+
</div>
|
|
109
|
+
<!-- Add more .project cards as needed. -->
|
|
110
|
+
<!-- PROJECTS_END -->
|
|
111
|
+
</div>
|
|
112
|
+
</section>
|
|
113
|
+
|
|
114
|
+
<section id="contact" class="contact">
|
|
115
|
+
<h2>Get in touch</h2>
|
|
116
|
+
<p style="color:var(--muted)">{{CONTACT_BODY}}</p>
|
|
117
|
+
<div class="actions">
|
|
118
|
+
<a class="btn" href="mailto:{{EMAIL}}">Email me</a>
|
|
119
|
+
<a class="btn ghost" href="{{PROFILE_LINK}}">{{PROFILE_LABEL}}</a>
|
|
120
|
+
</div>
|
|
121
|
+
</section>
|
|
122
|
+
|
|
123
|
+
<footer>{{FOOTER_TEXT}}</footer>
|
|
124
|
+
</div>
|
|
125
|
+
</body>
|
|
126
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Template: portfolio (static)
|
|
2
|
+
|
|
3
|
+
A single self-contained HTML personal / freelancer portfolio: a hero intro, an about section, a skills list, a projects grid, and a contact section. **No infrastructure** — no database, no backend, no `needs:`.
|
|
4
|
+
State (if any) lives only in memory and is lost on refresh.
|
|
5
|
+
|
|
6
|
+
## How it deploys
|
|
7
|
+
|
|
8
|
+
To publish this page, use **`gridctl_drop`** (or `gridctl_plug` with the inline
|
|
9
|
+
HTML): it deploys as an **inspiration** — instant, and works on the **hosted**
|
|
10
|
+
edition (Claude Web) as well as local. The reference `cloudgrid.yaml` in this
|
|
11
|
+
directory (`type: static`) is ONLY for plugging the page as an OWNED static
|
|
12
|
+
runtime (async, local edition only); the fast path is `gridctl_drop`.
|
|
13
|
+
|
|
14
|
+
The fillable HTML is the real template — fetch it with
|
|
15
|
+
`gridctl_fetch("template", "portfolio")` and fill in the user's content.
|
|
16
|
+
|
|
17
|
+
## cloudgrid.yaml (reference only)
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
# static — deploys as an inspiration (instant, works on hosted) via gridctl_drop;
|
|
21
|
+
# this yaml is only for plugging it as an OWNED static runtime (async, local
|
|
22
|
+
# edition only). To publish the page, prefer gridctl_drop, not grid plug.
|
|
23
|
+
#
|
|
24
|
+
# needs: {} # none — static, no infrastructure
|
|
25
|
+
name: portfolio
|
|
26
|
+
services:
|
|
27
|
+
web:
|
|
28
|
+
type: static
|
|
29
|
+
path: /
|
|
30
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# saas-marketing
|
|
2
|
+
|
|
3
|
+
A single self-contained HTML SaaS marketing site (hero, features, pricing tiers, testimonials, FAQ, and a closing call to action). Richer than the landing-page template.
|
|
4
|
+
|
|
5
|
+
Static inspiration — a single self-contained `index.html` (inline CSS and JS,
|
|
6
|
+
no external CDNs). It deploys instantly via `gridctl_drop` and works on every
|
|
7
|
+
CloudGrid edition, including hosted.
|
|
8
|
+
|
|
9
|
+
## Fill it in
|
|
10
|
+
|
|
11
|
+
Fetch the template with `gridctl_fetch("template", "saas-marketing")`, then replace the
|
|
12
|
+
placeholders:
|
|
13
|
+
|
|
14
|
+
- The product name, tagline, and hero eyebrow/headline/subheadline.
|
|
15
|
+
- The call-to-action label and (optional) target for the primary button.
|
|
16
|
+
- One block per feature (title + short description). Add or remove `.feature` blocks.
|
|
17
|
+
- The three pricing plans: name, price, period, and feature list. The middle plan is marked "Most popular".
|
|
18
|
+
- The testimonial quotes and who said them.
|
|
19
|
+
- The FAQ question/answer pairs (`<details>` blocks, client-side accordion).
|
|
20
|
+
|
|
21
|
+
Keep it a single self-contained HTML file. Do not add external scripts,
|
|
22
|
+
stylesheets, fonts, or large embedded media.
|
|
23
|
+
|
|
24
|
+
## Deploy
|
|
25
|
+
|
|
26
|
+
- Hosted MCP edition: call the drop tool with the HTML.
|
|
27
|
+
- Local MCP / CLI edition: write the HTML to a file and run `gridctl plug`.
|
|
28
|
+
|
|
29
|
+
It deploys as an inspiration and returns a live share URL right away. Re-deploy
|
|
30
|
+
with the same entity id to update the same URL.
|