@pantheon-systems/create-p1-starter-kit 0.1.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/index.js +5 -0
- package/lib/cli.js +149 -0
- package/lib/copy-template.js +67 -0
- package/lib/install-deps.js +68 -0
- package/lib/messages.js +28 -0
- package/package.json +38 -0
- package/template/.env.example +10 -0
- package/template/README.md +53 -0
- package/template/__tests__/editor-integration.test.ts +53 -0
- package/template/__tests__/remote-datasource-fetchers.test.ts +226 -0
- package/template/app/[...puckPath]/client.tsx +9 -0
- package/template/app/[...puckPath]/page.tsx +131 -0
- package/template/app/collection-nav.tsx +47 -0
- package/template/app/layout.tsx +13 -0
- package/template/app/p1/[[...p1]]/editor-client.tsx +116 -0
- package/template/app/p1/[[...p1]]/page.tsx +19 -0
- package/template/app/p1/[[...p1]]/render-client.tsx +9 -0
- package/template/app/p1/api/[...p1]/route.ts +16 -0
- package/template/app/p1/auth/[...action]/route.ts +9 -0
- package/template/app/p1/merge/merge-client.tsx +635 -0
- package/template/app/p1/merge/merge.css +257 -0
- package/template/app/p1/merge/page.tsx +12 -0
- package/template/app/page.tsx +130 -0
- package/template/app/styles.css +18 -0
- package/template/components/puck/block-padding.ts +2 -0
- package/template/components/puck/button-block.tsx +41 -0
- package/template/components/puck/divider-block.tsx +10 -0
- package/template/components/puck/grid-block.tsx +80 -0
- package/template/components/puck/heading-block.tsx +38 -0
- package/template/components/puck/image-block.tsx +33 -0
- package/template/components/puck/list-block.tsx +72 -0
- package/template/components/puck/paragraph-block.tsx +44 -0
- package/template/components/puck/quote-block.tsx +23 -0
- package/template/components/puck/root.tsx +20 -0
- package/template/components/puck/spacer-block.tsx +19 -0
- package/template/eslint.config.js +161 -0
- package/template/lib/content-publisher.ts +128 -0
- package/template/lib/fetcher-helpers.ts +17 -0
- package/template/lib/monsters-api.ts +125 -0
- package/template/lib/remote-datasource-fetchers.ts +10 -0
- package/template/lib/remote-datasources.ts +154 -0
- package/template/lib/swapi.ts +75 -0
- package/template/next-env.d.ts +6 -0
- package/template/next.config.mjs +13 -0
- package/template/package.json +42 -0
- package/template/postcss.config.mjs +8 -0
- package/template/public/sw.js +8 -0
- package/template/puck.config.tsx +51 -0
- package/template/tsconfig/base.json +20 -0
- package/template/tsconfig/nextjs.json +21 -0
- package/template/tsconfig.json +16 -0
- package/template/vitest.config.ts +7 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("@pantheon-systems/cpub-react-sdk/server", () => ({
|
|
4
|
+
PCCConvenienceFunctions: {
|
|
5
|
+
getPaginatedArticles: vi.fn(),
|
|
6
|
+
getArticleBySlugOrId: vi.fn(),
|
|
7
|
+
},
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
// We need to mock the user-remote-datasource-store since loadRemoteDatasourceContext uses it
|
|
11
|
+
vi.mock("@pantheon-systems/puck-css/server", async (importOriginal) => {
|
|
12
|
+
const actual = await importOriginal<Record<string, unknown>>();
|
|
13
|
+
return actual;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
import { REMOTE_DATASOURCE_FETCHERS } from "../lib/remote-datasource-fetchers";
|
|
17
|
+
import type { RemoteDatasourceFetcherParams } from "@pantheon-systems/puck-css/server";
|
|
18
|
+
|
|
19
|
+
const { PCCConvenienceFunctions } = await import(
|
|
20
|
+
"@pantheon-systems/cpub-react-sdk/server"
|
|
21
|
+
);
|
|
22
|
+
const mockGetPaginatedArticles = PCCConvenienceFunctions
|
|
23
|
+
.getPaginatedArticles as ReturnType<typeof vi.fn>;
|
|
24
|
+
const mockGetArticleBySlugOrId = PCCConvenienceFunctions
|
|
25
|
+
.getArticleBySlugOrId as ReturnType<typeof vi.fn>;
|
|
26
|
+
|
|
27
|
+
function makeFetcherParams(
|
|
28
|
+
overrides: Partial<RemoteDatasourceFetcherParams> = {},
|
|
29
|
+
): RemoteDatasourceFetcherParams {
|
|
30
|
+
return {
|
|
31
|
+
searchParams: {},
|
|
32
|
+
urlParams: {},
|
|
33
|
+
savedPreviewParams: {},
|
|
34
|
+
fetchImpl: vi.fn() as unknown as typeof fetch,
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getFetcher(id: string) {
|
|
40
|
+
const f = REMOTE_DATASOURCE_FETCHERS.find((f) => f.id === id);
|
|
41
|
+
if (!f) throw new Error(`No fetcher with id "${id}"`);
|
|
42
|
+
return f;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("swapi fetcher", () => {
|
|
46
|
+
const fetcher = getFetcher("swapi");
|
|
47
|
+
|
|
48
|
+
it("returns {} when no id is available", async () => {
|
|
49
|
+
const result = await fetcher.fetch(makeFetcherParams());
|
|
50
|
+
expect(result).toEqual({});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns {} on non-OK response", async () => {
|
|
54
|
+
const fetchImpl = vi.fn().mockResolvedValue({
|
|
55
|
+
ok: false,
|
|
56
|
+
status: 404,
|
|
57
|
+
json: async () => ({}),
|
|
58
|
+
});
|
|
59
|
+
const result = await fetcher.fetch(makeFetcherParams({
|
|
60
|
+
searchParams: { id: "1" },
|
|
61
|
+
fetchImpl,
|
|
62
|
+
}));
|
|
63
|
+
expect(result).toEqual({});
|
|
64
|
+
expect(fetchImpl).toHaveBeenCalledWith("https://swapi.info/api/people/1");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns parsed JSON on success", async () => {
|
|
68
|
+
const payload = { name: "Luke Skywalker", height: "172" };
|
|
69
|
+
const fetchImpl = vi.fn().mockResolvedValue({
|
|
70
|
+
ok: true,
|
|
71
|
+
json: async () => payload,
|
|
72
|
+
});
|
|
73
|
+
const result = await fetcher.fetch(makeFetcherParams({
|
|
74
|
+
searchParams: { id: "1" },
|
|
75
|
+
fetchImpl,
|
|
76
|
+
}));
|
|
77
|
+
expect(result).toEqual(payload);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("uses urlParams.id when searchParams has no id", async () => {
|
|
81
|
+
const fetchImpl = vi.fn().mockResolvedValue({
|
|
82
|
+
ok: true,
|
|
83
|
+
json: async () => ({ name: "FromPath" }),
|
|
84
|
+
});
|
|
85
|
+
const result = await fetcher.fetch(makeFetcherParams({
|
|
86
|
+
urlParams: { id: "5" },
|
|
87
|
+
fetchImpl,
|
|
88
|
+
}));
|
|
89
|
+
expect(result).toEqual({ name: "FromPath" });
|
|
90
|
+
expect(fetchImpl).toHaveBeenCalledWith("https://swapi.info/api/people/5");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("prefers query id over path id", async () => {
|
|
94
|
+
const fetchImpl = vi.fn().mockResolvedValue({
|
|
95
|
+
ok: true,
|
|
96
|
+
json: async () => ({ name: "Q" }),
|
|
97
|
+
});
|
|
98
|
+
await fetcher.fetch(makeFetcherParams({
|
|
99
|
+
searchParams: { id: "2" },
|
|
100
|
+
urlParams: { id: "9" },
|
|
101
|
+
fetchImpl,
|
|
102
|
+
}));
|
|
103
|
+
expect(fetchImpl).toHaveBeenCalledWith("https://swapi.info/api/people/2");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("swapi_list fetcher", () => {
|
|
108
|
+
const fetcher = getFetcher("swapi_list");
|
|
109
|
+
|
|
110
|
+
it("maps results to { items: [...] }", async () => {
|
|
111
|
+
const fetchImpl = vi.fn().mockResolvedValue({
|
|
112
|
+
ok: true,
|
|
113
|
+
json: async () => [
|
|
114
|
+
{ name: "Luke", url: "https://swapi.info/api/people/1" },
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
const result = await fetcher.fetch(makeFetcherParams({ fetchImpl }));
|
|
118
|
+
expect(result).toEqual({
|
|
119
|
+
items: [{ id: "1", name: "Luke", url: "https://swapi.info/api/people/1" }],
|
|
120
|
+
});
|
|
121
|
+
expect(fetchImpl).toHaveBeenCalledWith("https://swapi.info/api/people");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("monster fetcher", () => {
|
|
126
|
+
const fetcher = getFetcher("monster");
|
|
127
|
+
|
|
128
|
+
it("returns {} when no index available", async () => {
|
|
129
|
+
const result = await fetcher.fetch(makeFetcherParams());
|
|
130
|
+
expect(result).toEqual({});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("returns parsed pokemon on success", async () => {
|
|
134
|
+
const payload = {
|
|
135
|
+
data: {
|
|
136
|
+
getPokemon: {
|
|
137
|
+
key: "bulbasaur",
|
|
138
|
+
species: "Bulbasaur",
|
|
139
|
+
num: 1,
|
|
140
|
+
types: ["Grass", "Poison"],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
const fetchImpl = vi.fn().mockResolvedValue({
|
|
145
|
+
ok: true,
|
|
146
|
+
json: async () => payload,
|
|
147
|
+
});
|
|
148
|
+
const result = await fetcher.fetch(makeFetcherParams({
|
|
149
|
+
searchParams: { monster: "bulbasaur" },
|
|
150
|
+
fetchImpl,
|
|
151
|
+
}));
|
|
152
|
+
expect(result).toEqual({
|
|
153
|
+
key: "bulbasaur",
|
|
154
|
+
species: "Bulbasaur",
|
|
155
|
+
num: 1,
|
|
156
|
+
types: ["Grass", "Poison"],
|
|
157
|
+
index: "bulbasaur",
|
|
158
|
+
name: "Bulbasaur",
|
|
159
|
+
url: "/pokemon/bulbasaur",
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("monster_list fetcher", () => {
|
|
165
|
+
const fetcher = getFetcher("monster_list");
|
|
166
|
+
|
|
167
|
+
it("maps results to { items: [...] }", async () => {
|
|
168
|
+
const fetchImpl = vi.fn().mockResolvedValue({
|
|
169
|
+
ok: true,
|
|
170
|
+
json: async () => ({
|
|
171
|
+
data: {
|
|
172
|
+
getAllPokemon: [
|
|
173
|
+
{ key: "bulbasaur", species: "Bulbasaur" },
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
const result = await fetcher.fetch(makeFetcherParams({ fetchImpl }));
|
|
179
|
+
expect(result).toEqual({
|
|
180
|
+
items: [{ index: "bulbasaur", name: "Bulbasaur", url: "/pokemon/bulbasaur" }],
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("article fetcher", () => {
|
|
186
|
+
const fetcher = getFetcher("article");
|
|
187
|
+
|
|
188
|
+
it("returns {} when no article id", async () => {
|
|
189
|
+
const result = await fetcher.fetch(makeFetcherParams());
|
|
190
|
+
expect(result).toEqual({});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("loads a single article", async () => {
|
|
194
|
+
mockGetArticleBySlugOrId.mockResolvedValueOnce({
|
|
195
|
+
id: "first-article",
|
|
196
|
+
title: "First Article",
|
|
197
|
+
});
|
|
198
|
+
const result = await fetcher.fetch(makeFetcherParams({
|
|
199
|
+
searchParams: { article: "first-article" },
|
|
200
|
+
}));
|
|
201
|
+
expect(result).toEqual({ id: "first-article", title: "First Article" });
|
|
202
|
+
expect(mockGetArticleBySlugOrId).toHaveBeenCalledWith("first-article", {
|
|
203
|
+
contentType: "TEXT_MARKDOWN",
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("article_list fetcher", () => {
|
|
209
|
+
const fetcher = getFetcher("article_list");
|
|
210
|
+
|
|
211
|
+
it("maps list payload items to normalized article rows", async () => {
|
|
212
|
+
mockGetPaginatedArticles.mockResolvedValueOnce({
|
|
213
|
+
data: [
|
|
214
|
+
{ id: "a1", title: "First Article", slug: "first-article" },
|
|
215
|
+
{ id: 2, attributes: { title: "Second Article", slug: "second-article" } },
|
|
216
|
+
],
|
|
217
|
+
});
|
|
218
|
+
const result = await fetcher.fetch(makeFetcherParams());
|
|
219
|
+
expect(result).toEqual({
|
|
220
|
+
items: [
|
|
221
|
+
{ id: "a1", title: "First Article", slug: "first-article", url: undefined },
|
|
222
|
+
{ id: "2", title: "Second Article", slug: "second-article", url: undefined },
|
|
223
|
+
],
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { Data } from "@puckeditor/core";
|
|
4
|
+
import { RenderClient } from "@pantheon-systems/puck-css";
|
|
5
|
+
import config from "../../puck.config";
|
|
6
|
+
|
|
7
|
+
export function Client({ data }: { data: Data }) {
|
|
8
|
+
return <RenderClient config={config} data={data} />;
|
|
9
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catch-all route that renders user-facing pages generated by Puck.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Client } from "./client";
|
|
6
|
+
import { Metadata } from "next";
|
|
7
|
+
import {
|
|
8
|
+
loadRemoteDatasourceContext,
|
|
9
|
+
extractReferencedDatasourceIds,
|
|
10
|
+
getPage,
|
|
11
|
+
listRouteTemplateKeysFromDatabase,
|
|
12
|
+
resolveDataTemplates,
|
|
13
|
+
resolveStringTemplates,
|
|
14
|
+
ensureInitialized,
|
|
15
|
+
pagePathFromCatchAllSegments,
|
|
16
|
+
} from "@pantheon-systems/puck-css/server";
|
|
17
|
+
import { REMOTE_DATASOURCE_FETCHERS } from "../../lib/remote-datasource-fetchers";
|
|
18
|
+
|
|
19
|
+
const initPromise = ensureInitialized({
|
|
20
|
+
p1BaseUrl: process.env.NEXT_PUBLIC_CSS_BASE_URL,
|
|
21
|
+
p1ApiKey: process.env.P1_CSS_API_KEY,
|
|
22
|
+
p1SiteId: process.env.NEXT_PUBLIC_CSS_SITE_ID,
|
|
23
|
+
p1BranchId: process.env.NEXT_PUBLIC_CSS_BRANCH_ID,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export async function generateMetadata({
|
|
27
|
+
params,
|
|
28
|
+
searchParams,
|
|
29
|
+
}: {
|
|
30
|
+
params: Promise<{ puckPath: string[] }>;
|
|
31
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
32
|
+
}): Promise<Metadata> {
|
|
33
|
+
await initPromise;
|
|
34
|
+
const { puckPath = [] } = await params;
|
|
35
|
+
const path = pagePathFromCatchAllSegments(puckPath);
|
|
36
|
+
|
|
37
|
+
const [pageData, sp, routeTemplateKeys] = await Promise.all([
|
|
38
|
+
getPage(path),
|
|
39
|
+
searchParams,
|
|
40
|
+
listRouteTemplateKeysFromDatabase(),
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const rawTitle = pageData?.root.props?.title;
|
|
44
|
+
if (typeof rawTitle !== "string") {
|
|
45
|
+
return { title: rawTitle };
|
|
46
|
+
}
|
|
47
|
+
if (!rawTitle.includes("{{")) {
|
|
48
|
+
return { title: rawTitle };
|
|
49
|
+
}
|
|
50
|
+
const referencedDatasourceIds = extractReferencedDatasourceIds(pageData);
|
|
51
|
+
const context = await loadRemoteDatasourceContext({
|
|
52
|
+
searchParams: sp,
|
|
53
|
+
fetchImpl: fetch,
|
|
54
|
+
pagePath: path,
|
|
55
|
+
routeTemplateKeys,
|
|
56
|
+
builtinFetchers: REMOTE_DATASOURCE_FETCHERS,
|
|
57
|
+
referencedDatasourceIds,
|
|
58
|
+
});
|
|
59
|
+
return { title: await resolveStringTemplates(rawTitle, context) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default async function Page({
|
|
63
|
+
params,
|
|
64
|
+
searchParams,
|
|
65
|
+
}: {
|
|
66
|
+
params: Promise<{ puckPath: string[] }>;
|
|
67
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
68
|
+
}) {
|
|
69
|
+
await initPromise;
|
|
70
|
+
const { puckPath = [] } = await params;
|
|
71
|
+
const path = pagePathFromCatchAllSegments(puckPath);
|
|
72
|
+
|
|
73
|
+
const [data, searchParamData, routeTemplateKeys] = await Promise.all([
|
|
74
|
+
getPage(path),
|
|
75
|
+
searchParams,
|
|
76
|
+
listRouteTemplateKeysFromDatabase(),
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
if (!data) {
|
|
80
|
+
return (
|
|
81
|
+
<main className="flex min-h-screen items-center justify-center bg-gray-50">
|
|
82
|
+
<div className="mx-auto max-w-lg px-6 py-16 text-center">
|
|
83
|
+
<h1 className="text-3xl font-bold tracking-tight text-gray-900">
|
|
84
|
+
404 – This page doesn't exist yet
|
|
85
|
+
</h1>
|
|
86
|
+
<p className="mt-4 text-gray-600">
|
|
87
|
+
This page hasn't been created. Use the editor to build it.
|
|
88
|
+
</p>
|
|
89
|
+
|
|
90
|
+
<nav className="mt-10 flex flex-col gap-3">
|
|
91
|
+
<a
|
|
92
|
+
href={`/p1${path}`}
|
|
93
|
+
className="rounded-lg bg-gray-900 px-5 py-3 text-sm font-medium text-white hover:bg-gray-700"
|
|
94
|
+
>
|
|
95
|
+
Edit this page
|
|
96
|
+
</a>
|
|
97
|
+
<a
|
|
98
|
+
href="/p1"
|
|
99
|
+
className="rounded-lg border border-gray-300 px-5 py-3 text-sm font-medium text-gray-900 hover:bg-gray-100"
|
|
100
|
+
>
|
|
101
|
+
Open the Page Editor
|
|
102
|
+
</a>
|
|
103
|
+
<a
|
|
104
|
+
href="https://staging.content.pantheon.io/dashboard/sites"
|
|
105
|
+
target="_blank"
|
|
106
|
+
rel="noopener noreferrer"
|
|
107
|
+
className="rounded-lg border border-gray-300 px-5 py-3 text-sm font-medium text-gray-900 hover:bg-gray-100"
|
|
108
|
+
>
|
|
109
|
+
P1 Dashboard →
|
|
110
|
+
</a>
|
|
111
|
+
</nav>
|
|
112
|
+
</div>
|
|
113
|
+
</main>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const referencedDatasourceIds = extractReferencedDatasourceIds(data);
|
|
118
|
+
const context = await loadRemoteDatasourceContext({
|
|
119
|
+
searchParams: searchParamData,
|
|
120
|
+
fetchImpl: fetch,
|
|
121
|
+
pagePath: path,
|
|
122
|
+
routeTemplateKeys,
|
|
123
|
+
builtinFetchers: REMOTE_DATASOURCE_FETCHERS,
|
|
124
|
+
referencedDatasourceIds,
|
|
125
|
+
});
|
|
126
|
+
const resolvedData = await resolveDataTemplates(data, context);
|
|
127
|
+
|
|
128
|
+
return <Client data={resolvedData} />;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const dynamic = "force-dynamic";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
|
|
6
|
+
export function CollectionNav({ templatePath }: { templatePath: string }) {
|
|
7
|
+
const router = useRouter();
|
|
8
|
+
const segments = templatePath.split("/");
|
|
9
|
+
const params = segments
|
|
10
|
+
.filter((s) => s.startsWith(":"))
|
|
11
|
+
.map((s) => s.slice(1));
|
|
12
|
+
|
|
13
|
+
const [values, setValues] = useState<Record<string, string>>(
|
|
14
|
+
Object.fromEntries(params.map((p) => [p, ""])),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
function handleGo() {
|
|
18
|
+
if (!params.every((p) => values[p])) return;
|
|
19
|
+
const resolved = segments
|
|
20
|
+
.map((s) => (s.startsWith(":") ? encodeURIComponent(values[s.slice(1)]) : s))
|
|
21
|
+
.join("/");
|
|
22
|
+
router.push(resolved);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
27
|
+
<span className="text-sm text-gray-500 font-mono">{templatePath}</span>
|
|
28
|
+
{params.map((param) => (
|
|
29
|
+
<input
|
|
30
|
+
key={param}
|
|
31
|
+
type="text"
|
|
32
|
+
placeholder={param}
|
|
33
|
+
value={values[param]}
|
|
34
|
+
onChange={(e) => setValues({ ...values, [param]: e.target.value })}
|
|
35
|
+
onKeyDown={(e) => e.key === "Enter" && handleGo()}
|
|
36
|
+
className="rounded border border-gray-300 px-2 py-1 text-sm w-24"
|
|
37
|
+
/>
|
|
38
|
+
))}
|
|
39
|
+
<button
|
|
40
|
+
onClick={handleGo}
|
|
41
|
+
className="rounded bg-gray-900 px-3 py-1 text-sm text-white hover:bg-gray-700"
|
|
42
|
+
>
|
|
43
|
+
Go
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { Puck } from "@puckeditor/core";
|
|
6
|
+
import {
|
|
7
|
+
P1App,
|
|
8
|
+
createNextConfig,
|
|
9
|
+
useP1Editor,
|
|
10
|
+
useP1Plugins,
|
|
11
|
+
wrapConfigForEditorPreview,
|
|
12
|
+
P1QueryProvider,
|
|
13
|
+
} from "@pantheon-systems/puck-css";
|
|
14
|
+
import { P1NextRouterProvider } from "@pantheon-systems/p1-next-sdk";
|
|
15
|
+
import type { Checkpoint } from "@pantheon-systems/puck-css";
|
|
16
|
+
|
|
17
|
+
import "@pantheon-systems/puck-css/styles.css";
|
|
18
|
+
import "@pantheon-systems/puck-css/pds/styles.css";
|
|
19
|
+
|
|
20
|
+
import config from "../../../puck.config";
|
|
21
|
+
|
|
22
|
+
let p1Config: ReturnType<typeof createNextConfig> | null = null;
|
|
23
|
+
let p1ConfigError: string | null = null;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
p1Config = createNextConfig();
|
|
27
|
+
} catch (e) {
|
|
28
|
+
p1ConfigError = e instanceof Error ? e.message : String(e);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const editorConfig = wrapConfigForEditorPreview(config);
|
|
32
|
+
|
|
33
|
+
export function EditorClientWrapper({ path }: { path: string }) {
|
|
34
|
+
if (!p1Config) {
|
|
35
|
+
return (
|
|
36
|
+
<div style={{ textAlign: "center", padding: "4rem", fontFamily: "system-ui" }}>
|
|
37
|
+
<h3>Editor unavailable</h3>
|
|
38
|
+
<p style={{ color: "#666" }}>
|
|
39
|
+
{p1ConfigError ?? "P1 configuration is missing."}
|
|
40
|
+
</p>
|
|
41
|
+
<p style={{ color: "#888", fontSize: "14px" }}>
|
|
42
|
+
Set NEXT_PUBLIC_CSS_BASE_URL and NEXT_PUBLIC_CSS_SITE_ID environment
|
|
43
|
+
variables to enable the editor.
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<P1QueryProvider>
|
|
51
|
+
<P1NextRouterProvider>
|
|
52
|
+
<P1App
|
|
53
|
+
config={p1Config}
|
|
54
|
+
loginPageProps={{ title: "P1 Starter", subtitle: "Sign in to edit" }}
|
|
55
|
+
>
|
|
56
|
+
<EditorContent path={path} />
|
|
57
|
+
</P1App>
|
|
58
|
+
</P1NextRouterProvider>
|
|
59
|
+
</P1QueryProvider>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function EditorContent({ path }: { path: string }) {
|
|
64
|
+
const router = useRouter();
|
|
65
|
+
const p1Plugins = useP1Plugins(path, config);
|
|
66
|
+
|
|
67
|
+
const handleDocumentSelect = useCallback(
|
|
68
|
+
(docPath: string) => {
|
|
69
|
+
router.push(`/p1/${docPath}`);
|
|
70
|
+
},
|
|
71
|
+
[router],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const { loading, error, puckKey, puckProps } = useP1Editor({
|
|
75
|
+
documentPath: path,
|
|
76
|
+
puckConfig: editorConfig,
|
|
77
|
+
additionalPlugins: p1Plugins,
|
|
78
|
+
pluginOptions: {
|
|
79
|
+
onDocumentSelect: handleDocumentSelect,
|
|
80
|
+
selectedDocumentPath: path,
|
|
81
|
+
},
|
|
82
|
+
overrideOptions: {
|
|
83
|
+
showDefaultPublish: false,
|
|
84
|
+
onPublishSuccess: (checkpoint: Checkpoint) => {
|
|
85
|
+
alert(`Published: ${checkpoint.name ?? checkpoint.id}`);
|
|
86
|
+
},
|
|
87
|
+
onPublishError: (err: Error) => {
|
|
88
|
+
alert(`Publish failed: ${err.message}`);
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (loading) {
|
|
94
|
+
return (
|
|
95
|
+
<div style={{ textAlign: "center", padding: "4rem", fontFamily: "system-ui" }}>
|
|
96
|
+
Loading editor...
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (error) {
|
|
102
|
+
return (
|
|
103
|
+
<div style={{ textAlign: "center", padding: "4rem", fontFamily: "system-ui" }}>
|
|
104
|
+
<h3>Error loading document</h3>
|
|
105
|
+
<p style={{ color: "#666" }}>{error.message}</p>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className="puck-editor-theme">
|
|
112
|
+
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
113
|
+
<Puck key={puckKey} {...puckProps as any} _experimentalFullScreenCanvas={true} />
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "@puckeditor/core/puck.css";
|
|
2
|
+
import { createP1Pages } from "@pantheon-systems/p1-next-sdk/server";
|
|
3
|
+
import config from "../../../puck.config";
|
|
4
|
+
import { EditorClientWrapper } from "./editor-client";
|
|
5
|
+
import { RenderClientWrapper } from "./render-client";
|
|
6
|
+
|
|
7
|
+
const pages = createP1Pages({
|
|
8
|
+
config,
|
|
9
|
+
p1BaseUrl: process.env.NEXT_PUBLIC_CSS_BASE_URL,
|
|
10
|
+
p1ApiKey: process.env.P1_CSS_API_KEY,
|
|
11
|
+
p1SiteId: process.env.NEXT_PUBLIC_CSS_SITE_ID,
|
|
12
|
+
p1BranchId: process.env.NEXT_PUBLIC_CSS_BRANCH_ID,
|
|
13
|
+
EditorClient: EditorClientWrapper,
|
|
14
|
+
RenderClient: RenderClientWrapper,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export default pages.Page;
|
|
18
|
+
export const generateMetadata = pages.generateMetadata;
|
|
19
|
+
export const dynamic = "force-dynamic";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { Data } from "@puckeditor/core";
|
|
4
|
+
import { RenderClient } from "@pantheon-systems/puck-css";
|
|
5
|
+
import config from "../../../puck.config";
|
|
6
|
+
|
|
7
|
+
export function RenderClientWrapper({ data }: { data: Data }) {
|
|
8
|
+
return <RenderClient config={config} data={data} />;
|
|
9
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createP1Handler } from "@pantheon-systems/p1-next-sdk/server";
|
|
2
|
+
import config from "../../../../puck.config";
|
|
3
|
+
import { REMOTE_DATASOURCE_FETCHERS } from "../../../../lib/remote-datasource-fetchers";
|
|
4
|
+
import { REMOTE_DATASOURCE_REGISTRY } from "../../../../lib/remote-datasources";
|
|
5
|
+
|
|
6
|
+
const handler = createP1Handler({
|
|
7
|
+
config,
|
|
8
|
+
p1BaseUrl: process.env.NEXT_PUBLIC_CSS_BASE_URL,
|
|
9
|
+
p1ApiKey: process.env.P1_CSS_API_KEY,
|
|
10
|
+
p1SiteId: process.env.NEXT_PUBLIC_CSS_SITE_ID,
|
|
11
|
+
p1BranchId: process.env.NEXT_PUBLIC_CSS_BRANCH_ID,
|
|
12
|
+
builtinFetchers: REMOTE_DATASOURCE_FETCHERS,
|
|
13
|
+
builtinDatasourceRegistry: REMOTE_DATASOURCE_REGISTRY,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const { GET, POST, DELETE } = handler;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createP1AuthHandler } from "@pantheon-systems/p1-next-sdk/server";
|
|
2
|
+
|
|
3
|
+
const handler = createP1AuthHandler({
|
|
4
|
+
p1ApiKey: process.env.P1_CSS_API_KEY,
|
|
5
|
+
p1BaseUrl: process.env.NEXT_PUBLIC_CSS_BASE_URL,
|
|
6
|
+
prompt: 'login',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const { POST } = handler;
|