@pantheon-systems/create-p1-starter-kit 0.4.4 → 0.6.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 +1 -1
- package/template/.env.example +11 -0
- package/template/CHANGELOG.md +15 -0
- package/template/__tests__/auth-route.test.ts +28 -0
- package/template/__tests__/chatbot-flag-gate.test.ts +25 -0
- package/template/__tests__/chatbot-flag-wiring.test.ts +45 -0
- package/template/__tests__/editor-integration.test.ts +4 -0
- package/template/app/[...puckPath]/client.tsx +175 -0
- package/template/app/[...puckPath]/page.tsx +4 -2
- package/template/app/p1/[[...p1]]/editor-client.tsx +134 -5
- package/template/app/p1/[[...p1]]/page.tsx +3 -3
- package/template/app/p1/auth/[...action]/route.ts +1 -1
- package/template/app/page.tsx +18 -60
- package/template/components/ChatbotFlagProvider.tsx +49 -0
- package/template/components/p1-lockup.tsx +33 -0
- package/template/components/puck/welcome-block-render.tsx +110 -0
- package/template/components/puck/welcome-block.tsx +45 -0
- package/template/constants/assets.ts +3 -0
- package/template/lib/chatbot-flag/feature-gate.ts +17 -0
- package/template/package.json +5 -3
- package/template/public/images/p1_logo.svg +5 -0
- package/template/puck.config.tsx +6 -0
- package/template/app/p1/[[...p1]]/render-client.tsx +0 -9
package/package.json
CHANGED
package/template/.env.example
CHANGED
|
@@ -14,3 +14,14 @@ CSS_API_KEY=your-api-key
|
|
|
14
14
|
|
|
15
15
|
# Branch is auto-detected (defaults to main) unless specified:
|
|
16
16
|
# NEXT_PUBLIC_CSS_BRANCH_ID=branch-456
|
|
17
|
+
|
|
18
|
+
# --- Dev tools (disabled by default) ---
|
|
19
|
+
# Show the RoleSwitcher dropdown in the P1 editor for local testing of
|
|
20
|
+
# admin/editor/junior-editor permissions. Never enable this in production.
|
|
21
|
+
# NEXT_PUBLIC_ENABLE_ROLE_SWITCHER=true
|
|
22
|
+
|
|
23
|
+
# --- AI chatbot (optional) ---
|
|
24
|
+
# LaunchDarkly client-side ID used to evaluate the `p1-chatbot` flag. It is
|
|
25
|
+
# public by design (safe to expose in the browser). When unset, the chatbot
|
|
26
|
+
# stays hidden.
|
|
27
|
+
# NEXT_PUBLIC_LD_CLIENT_ID=your-launchdarkly-client-side-id
|
package/template/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @pantheon-systems/p1-starter
|
|
2
2
|
|
|
3
|
+
## 1.0.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @pantheon-systems/puck-css@0.6.0
|
|
8
|
+
- @pantheon-systems/p1-next-sdk@0.6.0
|
|
9
|
+
|
|
10
|
+
## 1.0.4
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [0bc7982]
|
|
15
|
+
- @pantheon-systems/puck-css@0.5.0
|
|
16
|
+
- @pantheon-systems/p1-next-sdk@0.5.0
|
|
17
|
+
|
|
3
18
|
## 1.0.3
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { resolve, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const appDir = resolve(__dirname, "..");
|
|
8
|
+
|
|
9
|
+
describe("auth route does not force re-authentication on every login", () => {
|
|
10
|
+
const content = readFileSync(
|
|
11
|
+
resolve(appDir, "app/p1/auth/[...action]/route.ts"),
|
|
12
|
+
"utf-8",
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
it("does not hardcode an OAuth prompt override", () => {
|
|
16
|
+
// prompt: 'login' forces Google's full re-auth screen on every broker
|
|
17
|
+
// login, even with a live Google session — see PCC-3391.
|
|
18
|
+
expect(content).not.toMatch(/prompt\s*:\s*['"]login['"]/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("uses select_account so users can still switch Google accounts", () => {
|
|
22
|
+
// Omitting `prompt` entirely silently re-authenticates whichever Google
|
|
23
|
+
// account has a live session, with no way to pick a different one on
|
|
24
|
+
// logout/login. select_account shows a lightweight account chooser
|
|
25
|
+
// (one click if already signed in) without forcing full re-auth.
|
|
26
|
+
expect(content).toMatch(/prompt\s*:\s*['"]select_account['"]/);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { shouldShowChatbot, CHATBOT_FLAG_KEY } from "../lib/chatbot-flag/feature-gate";
|
|
3
|
+
|
|
4
|
+
describe("shouldShowChatbot", () => {
|
|
5
|
+
it("is off when the flag is disabled, even with an agent URL", () => {
|
|
6
|
+
expect(shouldShowChatbot(false, "https://agent.example")).toBe(false);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("is off when the agent URL is missing, even with the flag enabled", () => {
|
|
10
|
+
expect(shouldShowChatbot(true, undefined)).toBe(false);
|
|
11
|
+
expect(shouldShowChatbot(true, "")).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("is on only when the flag is enabled AND the agent URL is set", () => {
|
|
15
|
+
expect(shouldShowChatbot(true, "https://agent.example")).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("defaults off when the flag value is undefined (LD not yet resolved / offline)", () => {
|
|
19
|
+
expect(shouldShowChatbot(undefined, "https://agent.example")).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("exposes the p1-chatbot flag key", () => {
|
|
23
|
+
expect(CHATBOT_FLAG_KEY).toBe("p1-chatbot");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { resolve, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const appDir = resolve(__dirname, "..");
|
|
8
|
+
|
|
9
|
+
describe("editor-client gates the chatbot behind the p1-chatbot flag", () => {
|
|
10
|
+
const content = readFileSync(
|
|
11
|
+
resolve(appDir, "app/p1/[[...p1]]/editor-client.tsx"),
|
|
12
|
+
"utf-8",
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
it("reads LaunchDarkly flags via useFlags", () => {
|
|
16
|
+
expect(content).toContain("useFlags");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("gates the AI plugin through shouldShowChatbot", () => {
|
|
20
|
+
expect(content).toContain("shouldShowChatbot");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("wraps the editor in the chatbot flag provider", () => {
|
|
24
|
+
expect(content).toContain("ChatbotFlagProvider");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("imports the plugin from the published @pantheon-systems/p1-ai-chat package", () => {
|
|
28
|
+
expect(content).toContain("@pantheon-systems/p1-ai-chat");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("chatbot flag provider is a client-side LaunchDarkly gate", () => {
|
|
33
|
+
const content = readFileSync(
|
|
34
|
+
resolve(appDir, "components/ChatbotFlagProvider.tsx"),
|
|
35
|
+
"utf-8",
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
it("initializes from the public client-side ID env var", () => {
|
|
39
|
+
expect(content).toContain("NEXT_PUBLIC_LD_CLIENT_ID");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("uses the launchdarkly-react-client-sdk", () => {
|
|
43
|
+
expect(content).toContain("launchdarkly-react-client-sdk");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -27,6 +27,10 @@ describe("editor-client uses P1 plugins", () => {
|
|
|
27
27
|
it("wraps with P1QueryProvider for TanStack React Query", () => {
|
|
28
28
|
expect(content).toContain("P1QueryProvider");
|
|
29
29
|
});
|
|
30
|
+
|
|
31
|
+
it("gates RoleSwitcher behind NEXT_PUBLIC_ENABLE_ROLE_SWITCHER", () => {
|
|
32
|
+
expect(content).toContain("NEXT_PUBLIC_ENABLE_ROLE_SWITCHER");
|
|
33
|
+
});
|
|
30
34
|
});
|
|
31
35
|
|
|
32
36
|
describe("API handler passes fetcher config", () => {
|
|
@@ -1,9 +1,183 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
3
4
|
import type { Data } from "@puckeditor/core";
|
|
4
5
|
import { RenderClient } from "@pantheon-systems/puck-css";
|
|
5
6
|
import config from "../../puck.config";
|
|
6
7
|
|
|
8
|
+
function EditIcon() {
|
|
9
|
+
return (
|
|
10
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" width={16} height={16} aria-hidden>
|
|
11
|
+
<path d="M12 20h9" />
|
|
12
|
+
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" />
|
|
13
|
+
</svg>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ChevronIcon() {
|
|
18
|
+
return (
|
|
19
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" width={14} height={14} aria-hidden>
|
|
20
|
+
<path d="M6 9l6 6 6-6" />
|
|
21
|
+
</svg>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function LogoutIcon() {
|
|
26
|
+
return (
|
|
27
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" width={16} height={16} aria-hidden>
|
|
28
|
+
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
|
29
|
+
<polyline points="16 17 21 12 16 7" />
|
|
30
|
+
<line x1="21" y1="12" x2="9" y2="12" />
|
|
31
|
+
</svg>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function P1EditWidget({ route }: { route: string }) {
|
|
36
|
+
const [hasToken, setHasToken] = useState(false);
|
|
37
|
+
const [open, setOpen] = useState(false);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const flag = typeof localStorage !== "undefined"
|
|
41
|
+
? localStorage.getItem("p1_logged_in")
|
|
42
|
+
: null;
|
|
43
|
+
setHasToken(!!flag);
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!open) return;
|
|
48
|
+
const onDoc = (e: PointerEvent) => {
|
|
49
|
+
if (!(e.target as HTMLElement).closest("[data-p1-widget]")) setOpen(false);
|
|
50
|
+
};
|
|
51
|
+
const onKey = (e: KeyboardEvent) => {
|
|
52
|
+
if (e.key === "Escape") setOpen(false);
|
|
53
|
+
};
|
|
54
|
+
document.addEventListener("pointerdown", onDoc);
|
|
55
|
+
document.addEventListener("keydown", onKey);
|
|
56
|
+
return () => {
|
|
57
|
+
document.removeEventListener("pointerdown", onDoc);
|
|
58
|
+
document.removeEventListener("keydown", onKey);
|
|
59
|
+
};
|
|
60
|
+
}, [open]);
|
|
61
|
+
|
|
62
|
+
if (!hasToken) return null;
|
|
63
|
+
|
|
64
|
+
const editHref = `/p1${route === "/" ? "" : route}`;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
data-p1-widget
|
|
69
|
+
style={{
|
|
70
|
+
position: "fixed",
|
|
71
|
+
top: 16,
|
|
72
|
+
right: 16,
|
|
73
|
+
zIndex: 99999,
|
|
74
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<button
|
|
78
|
+
onClick={() => setOpen((o) => !o)}
|
|
79
|
+
aria-expanded={open}
|
|
80
|
+
aria-haspopup="menu"
|
|
81
|
+
style={{
|
|
82
|
+
display: "inline-flex",
|
|
83
|
+
alignItems: "center",
|
|
84
|
+
gap: 8,
|
|
85
|
+
height: 38,
|
|
86
|
+
padding: "4px 12px 4px 12px",
|
|
87
|
+
background: "#fff",
|
|
88
|
+
border: "1px solid #e0e0e0",
|
|
89
|
+
borderRadius: 999,
|
|
90
|
+
cursor: "pointer",
|
|
91
|
+
fontSize: 14,
|
|
92
|
+
fontWeight: 500,
|
|
93
|
+
color: "#1a1a1a",
|
|
94
|
+
boxShadow: "0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.06)",
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
<span style={{ fontWeight: 600 }}>P1</span>
|
|
98
|
+
<span
|
|
99
|
+
style={{
|
|
100
|
+
display: "inline-flex",
|
|
101
|
+
color: "#888",
|
|
102
|
+
transition: "transform 200ms ease",
|
|
103
|
+
transform: open ? "rotate(180deg)" : "none",
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
<ChevronIcon />
|
|
107
|
+
</span>
|
|
108
|
+
</button>
|
|
109
|
+
|
|
110
|
+
{open && (
|
|
111
|
+
<div
|
|
112
|
+
role="menu"
|
|
113
|
+
style={{
|
|
114
|
+
position: "absolute",
|
|
115
|
+
top: "calc(100% + 8px)",
|
|
116
|
+
right: 0,
|
|
117
|
+
minWidth: 200,
|
|
118
|
+
background: "#fff",
|
|
119
|
+
border: "1px solid #e0e0e0",
|
|
120
|
+
borderRadius: 8,
|
|
121
|
+
boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
|
|
122
|
+
padding: 6,
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
<a
|
|
126
|
+
href={editHref}
|
|
127
|
+
role="menuitem"
|
|
128
|
+
style={{
|
|
129
|
+
display: "flex",
|
|
130
|
+
alignItems: "center",
|
|
131
|
+
gap: 10,
|
|
132
|
+
width: "100%",
|
|
133
|
+
padding: "9px 10px",
|
|
134
|
+
borderRadius: 4,
|
|
135
|
+
fontSize: 14,
|
|
136
|
+
color: "#1a1a1a",
|
|
137
|
+
textDecoration: "none",
|
|
138
|
+
}}
|
|
139
|
+
onMouseEnter={(e) => { e.currentTarget.style.background = "#f5f5f5"; }}
|
|
140
|
+
onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; }}
|
|
141
|
+
>
|
|
142
|
+
<span style={{ color: "#888", flex: "0 0 auto" }}><EditIcon /></span>
|
|
143
|
+
Edit this page
|
|
144
|
+
</a>
|
|
145
|
+
<div style={{ height: 1, background: "#e0e0e0", margin: "6px 4px" }} />
|
|
146
|
+
<button
|
|
147
|
+
role="menuitem"
|
|
148
|
+
onClick={() => {
|
|
149
|
+
localStorage.removeItem("p1_logged_in");
|
|
150
|
+
localStorage.removeItem("p1_auth_token");
|
|
151
|
+
localStorage.removeItem("css_broker_token");
|
|
152
|
+
window.location.reload();
|
|
153
|
+
}}
|
|
154
|
+
style={{
|
|
155
|
+
display: "flex",
|
|
156
|
+
alignItems: "center",
|
|
157
|
+
gap: 10,
|
|
158
|
+
width: "100%",
|
|
159
|
+
padding: "9px 10px",
|
|
160
|
+
borderRadius: 4,
|
|
161
|
+
fontSize: 14,
|
|
162
|
+
color: "#1a1a1a",
|
|
163
|
+
textDecoration: "none",
|
|
164
|
+
background: "transparent",
|
|
165
|
+
border: "none",
|
|
166
|
+
cursor: "pointer",
|
|
167
|
+
fontFamily: "inherit",
|
|
168
|
+
}}
|
|
169
|
+
onMouseEnter={(e) => { e.currentTarget.style.background = "#f5f5f5"; }}
|
|
170
|
+
onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; }}
|
|
171
|
+
>
|
|
172
|
+
<span style={{ color: "#888", flex: "0 0 auto" }}><LogoutIcon /></span>
|
|
173
|
+
Log out
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
7
181
|
export function Client({
|
|
8
182
|
data,
|
|
9
183
|
pageMetadata,
|
|
@@ -18,6 +192,7 @@ export function Client({
|
|
|
18
192
|
return (
|
|
19
193
|
<>
|
|
20
194
|
<RenderClient config={config} data={data} />
|
|
195
|
+
{pageMetadata && <P1EditWidget route={pageMetadata.route} />}
|
|
21
196
|
{pageMetadata && (
|
|
22
197
|
<footer className="mt-16 border-t border-gray-200 py-4 text-center text-sm text-gray-500">
|
|
23
198
|
Rendered with{" "}
|
|
@@ -20,7 +20,9 @@ const initPromise = ensureInitialized({
|
|
|
20
20
|
p1BaseUrl: process.env.NEXT_PUBLIC_CSS_BASE_URL,
|
|
21
21
|
p1ApiKey: process.env.CSS_API_KEY,
|
|
22
22
|
p1SiteId: process.env.NEXT_PUBLIC_CSS_SITE_ID,
|
|
23
|
-
|
|
23
|
+
// Default to "main" when unset: server components (no user token) need a
|
|
24
|
+
// branch to list/read documents (e.g. the /structure routes table).
|
|
25
|
+
p1BranchId: process.env.NEXT_PUBLIC_CSS_BRANCH_ID ?? "main",
|
|
24
26
|
});
|
|
25
27
|
|
|
26
28
|
export async function generateMetadata({
|
|
@@ -110,7 +112,7 @@ export default async function Page({
|
|
|
110
112
|
Open the Page Editor
|
|
111
113
|
</a>
|
|
112
114
|
<a
|
|
113
|
-
href="https://
|
|
115
|
+
href={`${process.env.NEXT_PUBLIC_P1_ADMIN_DASHBOARD_URL || "https://content.pantheon.io"}/dashboard/sites`}
|
|
114
116
|
target="_blank"
|
|
115
117
|
rel="noopener noreferrer"
|
|
116
118
|
className="rounded-lg border border-gray-300 px-5 py-3 text-sm font-medium text-gray-900 hover:bg-gray-100"
|
|
@@ -8,18 +8,56 @@ import {
|
|
|
8
8
|
createNextConfig,
|
|
9
9
|
useP1Editor,
|
|
10
10
|
useP1Plugins,
|
|
11
|
+
useP1Auth,
|
|
11
12
|
wrapConfigForEditorPreview,
|
|
12
13
|
P1QueryProvider,
|
|
13
14
|
editorPathHref,
|
|
14
15
|
} from "@pantheon-systems/puck-css";
|
|
15
16
|
import { P1NextRouterProvider } from "@pantheon-systems/p1-next-sdk";
|
|
17
|
+
import { createAIChatPlugin } from "@pantheon-systems/p1-ai-chat";
|
|
18
|
+
import { useFlags } from "launchdarkly-react-client-sdk";
|
|
16
19
|
import type { Checkpoint } from "@pantheon-systems/puck-css";
|
|
17
20
|
import type { ContentRole } from "@pantheon-systems/puck-css";
|
|
21
|
+
import { P1_ASSETS } from "../../../constants/assets";
|
|
18
22
|
|
|
19
23
|
import "@pantheon-systems/puck-css/styles.css";
|
|
20
24
|
import "@pantheon-systems/puck-css/pds/styles.css";
|
|
21
25
|
|
|
26
|
+
import { ChatbotFlagProvider } from "../../../components/ChatbotFlagProvider";
|
|
27
|
+
import { P1Lockup } from "../../../components/p1-lockup";
|
|
22
28
|
import config from "../../../puck.config";
|
|
29
|
+
import { shouldShowChatbot, CHATBOT_FLAG_KEY } from "../../../lib/chatbot-flag/feature-gate";
|
|
30
|
+
|
|
31
|
+
const DEFAULT_PAGE_DATA = {
|
|
32
|
+
root: { props: { title: "New page" } },
|
|
33
|
+
content: [],
|
|
34
|
+
zones: {},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const DEFAULT_ROOT_PAGE_DATA = {
|
|
38
|
+
root: { props: { title: "Welcome | P1 site" } },
|
|
39
|
+
content: [
|
|
40
|
+
{
|
|
41
|
+
type: "P1WelcomeBlock",
|
|
42
|
+
props: {
|
|
43
|
+
id: "seed-welcome",
|
|
44
|
+
heading: "Welcome to your new Pantheon P1 Site.",
|
|
45
|
+
description: "You just created this new site from Pantheon P1 starter kit, congrats! You'll need a Pantheon P1 user account to edit it and create new pages.",
|
|
46
|
+
ctaLabel: "Sign-in to P1",
|
|
47
|
+
ctaHref: "/p1",
|
|
48
|
+
footnote: "Visit [P1 documentation](https://docs.pantheon.io) for more information.",
|
|
49
|
+
loggedInHeading: "Welcome to your new Pantheon P1 Site.",
|
|
50
|
+
loggedInDescription: "You just created this new site from Pantheon P1 starter kit, congrats! Start editing this page or visit the P1 dashboard to manage your site.",
|
|
51
|
+
loggedInCtaLabel: "Edit this page with P1 Visual Editor",
|
|
52
|
+
loggedInCtaHref: "/p1",
|
|
53
|
+
loggedInSecondaryLabel: "Go to P1 Dashboard",
|
|
54
|
+
loggedInFootnote: "Visit [P1 documentation](https://docs.pantheon.io) for more information.",
|
|
55
|
+
showLogo: true,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
zones: {},
|
|
60
|
+
};
|
|
23
61
|
|
|
24
62
|
let p1Config: ReturnType<typeof createNextConfig> | null = null;
|
|
25
63
|
let p1ConfigError: string | null = null;
|
|
@@ -34,6 +72,41 @@ const editorConfig = wrapConfigForEditorPreview(config);
|
|
|
34
72
|
|
|
35
73
|
const ROLES: ContentRole[] = ['admin', 'editor', 'junior-editor'];
|
|
36
74
|
|
|
75
|
+
function P1SignInPage() {
|
|
76
|
+
const { login, isLoading, error } = useP1Auth();
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="w-full max-w-[620px] mx-auto flex flex-col items-center text-center px-8 py-16 font-['Inter',system-ui,sans-serif] text-[#1a1a2e] min-h-screen justify-center">
|
|
80
|
+
<P1Lockup />
|
|
81
|
+
|
|
82
|
+
<h1 className="text-[2.5rem] leading-[1.08] font-semibold m-0 mb-3" style={{ fontSize: '2.5rem' }}>
|
|
83
|
+
Your Collaborative Website Management Workspace.
|
|
84
|
+
</h1>
|
|
85
|
+
<p className="text-base leading-6 text-[#5a5a6e] max-w-[54ch] m-0">
|
|
86
|
+
Log in to your Pantheon P1 account to edit your P1 powered website.
|
|
87
|
+
If you don't have yet a Pantheon P1 account, contact us{" "}
|
|
88
|
+
<a href="https://pantheon.io/contact-us" className="text-blue-600 underline">here</a>.
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
<div className="flex gap-3 mt-8 justify-center">
|
|
92
|
+
<button
|
|
93
|
+
className="inline-flex items-center justify-center h-12 px-6 gap-2 rounded-full border border-[#1a1a2e] bg-[#1a1a2e] text-white font-['Inter',system-ui,sans-serif] text-lg font-medium leading-none whitespace-nowrap cursor-pointer transition-colors duration-200 hover:bg-[#2d2d44] hover:border-[#2d2d44] focus-visible:outline focus-visible:outline-1 focus-visible:outline-blue-600 focus-visible:outline-offset-1 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
94
|
+
onClick={() => void login()}
|
|
95
|
+
disabled={isLoading}
|
|
96
|
+
>
|
|
97
|
+
{isLoading ? "Signing in..." : "Continue"}
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{error && (
|
|
102
|
+
<p className="mt-4 text-sm text-red-600 bg-red-50 px-3 py-2 rounded-md">
|
|
103
|
+
{error}
|
|
104
|
+
</p>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
37
110
|
export function EditorClientWrapper({ path }: { path: string }) {
|
|
38
111
|
const [userRole, setUserRole] = useState<ContentRole>('editor');
|
|
39
112
|
const lastGoodStateRef = React.useRef<{ puckKey: string; puckProps: any } | null>(null);
|
|
@@ -58,11 +131,15 @@ export function EditorClientWrapper({ path }: { path: string }) {
|
|
|
58
131
|
<P1NextRouterProvider>
|
|
59
132
|
<P1App
|
|
60
133
|
config={{ ...p1Config, userRole }}
|
|
61
|
-
|
|
134
|
+
loginFallback={<P1SignInPage />}
|
|
62
135
|
>
|
|
63
|
-
<
|
|
136
|
+
<ChatbotFlagProvider>
|
|
137
|
+
<EditorContent path={path} lastGoodStateRef={lastGoodStateRef} />
|
|
138
|
+
</ChatbotFlagProvider>
|
|
64
139
|
</P1App>
|
|
65
|
-
|
|
140
|
+
{process.env.NEXT_PUBLIC_ENABLE_ROLE_SWITCHER === 'true' && (
|
|
141
|
+
<RoleSwitcher currentRole={userRole} onRoleChange={setUserRole} />
|
|
142
|
+
)}
|
|
66
143
|
</P1NextRouterProvider>
|
|
67
144
|
</P1QueryProvider>
|
|
68
145
|
);
|
|
@@ -126,7 +203,33 @@ function EditorContent({
|
|
|
126
203
|
lastGoodStateRef: React.MutableRefObject<{ puckKey: string; puckProps: any } | null>;
|
|
127
204
|
}) {
|
|
128
205
|
const router = useRouter();
|
|
206
|
+
const { getToken } = useP1Auth();
|
|
129
207
|
const p1Plugins = useP1Plugins(path, config);
|
|
208
|
+
const flags = useFlags();
|
|
209
|
+
const agentUrl = process.env.NEXT_PUBLIC_AGENT_URL;
|
|
210
|
+
const chatbotEnabled = shouldShowChatbot(flags[CHATBOT_FLAG_KEY], agentUrl);
|
|
211
|
+
const aiPlugin = React.useMemo(
|
|
212
|
+
() =>
|
|
213
|
+
chatbotEnabled && agentUrl
|
|
214
|
+
? createAIChatPlugin({ agentUrl })
|
|
215
|
+
: null,
|
|
216
|
+
[chatbotEnabled, agentUrl],
|
|
217
|
+
);
|
|
218
|
+
const additionalPlugins = React.useMemo(
|
|
219
|
+
() => (aiPlugin ? [...p1Plugins, aiPlugin] : p1Plugins),
|
|
220
|
+
[p1Plugins, aiPlugin],
|
|
221
|
+
) as typeof p1Plugins;
|
|
222
|
+
|
|
223
|
+
const [redirecting, setRedirecting] = React.useState(false);
|
|
224
|
+
|
|
225
|
+
React.useEffect(() => {
|
|
226
|
+
const returnTo = localStorage.getItem("p1_return_to");
|
|
227
|
+
if (returnTo) {
|
|
228
|
+
localStorage.removeItem("p1_return_to");
|
|
229
|
+
setRedirecting(true);
|
|
230
|
+
router.push(returnTo);
|
|
231
|
+
}
|
|
232
|
+
}, [router]);
|
|
130
233
|
|
|
131
234
|
const handleDocumentSelect = useCallback(
|
|
132
235
|
(docPath: string) => {
|
|
@@ -135,15 +238,33 @@ function EditorContent({
|
|
|
135
238
|
[router],
|
|
136
239
|
);
|
|
137
240
|
|
|
241
|
+
const handleDocumentNotFound = useCallback(
|
|
242
|
+
async (docPath: string, _error: Error) => {
|
|
243
|
+
const initialData = docPath === "/" ? DEFAULT_ROOT_PAGE_DATA : DEFAULT_PAGE_DATA;
|
|
244
|
+
const token = await getToken();
|
|
245
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
246
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
247
|
+
const res = await fetch("/p1/api/structure/page", {
|
|
248
|
+
method: "POST",
|
|
249
|
+
headers,
|
|
250
|
+
body: JSON.stringify({ path: docPath, initialData }),
|
|
251
|
+
});
|
|
252
|
+
return res.ok;
|
|
253
|
+
},
|
|
254
|
+
[getToken],
|
|
255
|
+
);
|
|
256
|
+
|
|
138
257
|
const { loading, error, puckKey, puckProps } = useP1Editor({
|
|
139
258
|
documentPath: path,
|
|
140
259
|
puckConfig: editorConfig,
|
|
141
|
-
additionalPlugins
|
|
260
|
+
additionalPlugins,
|
|
261
|
+
onDocumentNotFound: handleDocumentNotFound,
|
|
142
262
|
pluginOptions: {
|
|
143
263
|
onDocumentSelect: handleDocumentSelect,
|
|
144
264
|
selectedDocumentPath: path,
|
|
145
265
|
siteId: process.env.NEXT_PUBLIC_CSS_SITE_ID,
|
|
146
266
|
dashboardUrl: process.env.NEXT_PUBLIC_P1_ADMIN_DASHBOARD_URL,
|
|
267
|
+
logoUrl: P1_ASSETS.LOGO_URL,
|
|
147
268
|
},
|
|
148
269
|
overrideOptions: {
|
|
149
270
|
showDefaultPublish: false,
|
|
@@ -163,6 +284,14 @@ function EditorContent({
|
|
|
163
284
|
}
|
|
164
285
|
}, [loading, error, puckKey, puckProps]);
|
|
165
286
|
|
|
287
|
+
if (redirecting) {
|
|
288
|
+
return (
|
|
289
|
+
<div style={{ textAlign: "center", padding: "4rem", fontFamily: "system-ui" }}>
|
|
290
|
+
Redirecting...
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
166
295
|
// Show full loading screen only on first load (no previous state)
|
|
167
296
|
if (loading && !lastGoodStateRef.current) {
|
|
168
297
|
return (
|
|
@@ -230,7 +359,7 @@ function EditorContent({
|
|
|
230
359
|
</div>
|
|
231
360
|
)}
|
|
232
361
|
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
233
|
-
<Puck key={displayState.puckKey} {...displayState.puckProps as any} _experimentalFullScreenCanvas={true} />
|
|
362
|
+
<Puck key={`${displayState.puckKey}-${chatbotEnabled ? "ai" : "no-ai"}`} {...displayState.puckProps as any} _experimentalFullScreenCanvas={true} />
|
|
234
363
|
</div>
|
|
235
364
|
);
|
|
236
365
|
}
|
|
@@ -2,16 +2,16 @@ import "@puckeditor/core/puck.css";
|
|
|
2
2
|
import { createP1Pages } from "@pantheon-systems/p1-next-sdk/server";
|
|
3
3
|
import config from "../../../puck.config";
|
|
4
4
|
import { EditorClientWrapper } from "./editor-client";
|
|
5
|
-
import { RenderClientWrapper } from "./render-client";
|
|
6
5
|
|
|
7
6
|
const pages = createP1Pages({
|
|
8
7
|
config,
|
|
9
8
|
p1BaseUrl: process.env.NEXT_PUBLIC_CSS_BASE_URL,
|
|
10
9
|
p1ApiKey: process.env.CSS_API_KEY,
|
|
11
10
|
p1SiteId: process.env.NEXT_PUBLIC_CSS_SITE_ID,
|
|
12
|
-
|
|
11
|
+
// Default to "main" when unset: server components (no user token) need a
|
|
12
|
+
// branch to list/read documents (e.g. the /p1/structure routes table).
|
|
13
|
+
p1BranchId: process.env.NEXT_PUBLIC_CSS_BRANCH_ID ?? "main",
|
|
13
14
|
EditorClient: EditorClientWrapper,
|
|
14
|
-
RenderClient: RenderClientWrapper,
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
export default pages.Page;
|
|
@@ -3,7 +3,7 @@ import { createP1AuthHandler } from "@pantheon-systems/p1-next-sdk/server";
|
|
|
3
3
|
const handler = createP1AuthHandler({
|
|
4
4
|
p1ApiKey: process.env.CSS_API_KEY,
|
|
5
5
|
p1BaseUrl: process.env.NEXT_PUBLIC_CSS_BASE_URL,
|
|
6
|
-
prompt:
|
|
6
|
+
prompt: "select_account",
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
export const { POST } = handler;
|
package/template/app/page.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { WelcomeBlockRender } from "../components/puck/welcome-block-render";
|
|
2
2
|
import {
|
|
3
|
-
listRoutes,
|
|
4
3
|
ensureInitialized,
|
|
5
4
|
getPage,
|
|
6
5
|
listRouteTemplateKeysFromDatabase,
|
|
@@ -12,13 +11,14 @@ import {
|
|
|
12
11
|
import type { Metadata } from "next";
|
|
13
12
|
import { REMOTE_DATASOURCE_FETCHERS } from "../lib/remote-datasource-fetchers";
|
|
14
13
|
import { Client } from "./[...puckPath]/client";
|
|
15
|
-
import { CollectionNav } from "./collection-nav";
|
|
16
14
|
|
|
17
15
|
const initPromise = ensureInitialized({
|
|
18
16
|
p1BaseUrl: process.env.NEXT_PUBLIC_CSS_BASE_URL,
|
|
19
17
|
p1ApiKey: process.env.CSS_API_KEY,
|
|
20
18
|
p1SiteId: process.env.NEXT_PUBLIC_CSS_SITE_ID,
|
|
21
|
-
|
|
19
|
+
// Default to "main" when unset: server components (no user token) need a
|
|
20
|
+
// branch to list/read documents (e.g. the /structure routes table).
|
|
21
|
+
p1BranchId: process.env.NEXT_PUBLIC_CSS_BRANCH_ID ?? "main",
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
export async function generateMetadata(): Promise<Metadata> {
|
|
@@ -76,63 +76,21 @@ export default async function HomePage() {
|
|
|
76
76
|
);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
const routes = await listRoutes();
|
|
80
|
-
|
|
81
|
-
const staticPages = routes.filter((r) => r.kind === "static");
|
|
82
|
-
const templates = routes.filter((r) => r.kind === "template");
|
|
83
|
-
|
|
84
79
|
return (
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
>
|
|
100
|
-
Open the Page Editor
|
|
101
|
-
</Link>
|
|
102
|
-
<a
|
|
103
|
-
href="https://staging.content.pantheon.io/dashboard/sites"
|
|
104
|
-
target="_blank"
|
|
105
|
-
rel="noopener noreferrer"
|
|
106
|
-
className="rounded-lg border border-gray-300 px-5 py-3 text-sm font-medium text-gray-900 hover:bg-gray-100"
|
|
107
|
-
>
|
|
108
|
-
P1 Dashboard →
|
|
109
|
-
</a>
|
|
110
|
-
</nav>
|
|
111
|
-
|
|
112
|
-
{(staticPages.length > 0 || templates.length > 0) && (
|
|
113
|
-
<div className="mt-10 text-left">
|
|
114
|
-
<h2 className="text-lg font-semibold text-gray-900 mb-4">Pages</h2>
|
|
115
|
-
<ul className="space-y-3">
|
|
116
|
-
{staticPages.map((route) => (
|
|
117
|
-
<li key={route.path}>
|
|
118
|
-
<Link
|
|
119
|
-
href={route.path}
|
|
120
|
-
className="text-sm font-mono text-blue-600 hover:underline"
|
|
121
|
-
>
|
|
122
|
-
{route.path}
|
|
123
|
-
</Link>
|
|
124
|
-
</li>
|
|
125
|
-
))}
|
|
126
|
-
{templates.map((route) => (
|
|
127
|
-
<li key={route.path}>
|
|
128
|
-
<CollectionNav templatePath={route.path} />
|
|
129
|
-
</li>
|
|
130
|
-
))}
|
|
131
|
-
</ul>
|
|
132
|
-
</div>
|
|
133
|
-
)}
|
|
134
|
-
</div>
|
|
135
|
-
</main>
|
|
80
|
+
<WelcomeBlockRender
|
|
81
|
+
heading="Welcome to your new Pantheon P1 Site."
|
|
82
|
+
description="You just created this new site from Pantheon P1 starter kit, congrats! You'll need a Pantheon P1 user account to edit it and create new pages."
|
|
83
|
+
ctaLabel="Sign-in to P1"
|
|
84
|
+
ctaHref="/p1"
|
|
85
|
+
footnote="Visit [P1 documentation](https://docs.pantheon.io) for more information."
|
|
86
|
+
loggedInHeading="Welcome to your new Pantheon P1 Site."
|
|
87
|
+
loggedInDescription="You just created this new site from Pantheon P1 starter kit, congrats! Start editing this page or visit the P1 dashboard to manage your site."
|
|
88
|
+
loggedInCtaLabel="Edit this page with P1 Visual Editor"
|
|
89
|
+
loggedInCtaHref="/p1"
|
|
90
|
+
loggedInSecondaryLabel="Go to P1 Dashboard"
|
|
91
|
+
loggedInFootnote="Visit [P1 documentation](https://docs.pantheon.io) for more information."
|
|
92
|
+
showLogo={true}
|
|
93
|
+
/>
|
|
136
94
|
);
|
|
137
95
|
}
|
|
138
96
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { LDProvider } from "launchdarkly-react-client-sdk";
|
|
5
|
+
import { useP1Auth } from "@pantheon-systems/puck-css";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Wraps the editor with a LaunchDarkly client-side provider so the `p1-chatbot`
|
|
9
|
+
* flag can be evaluated at runtime. The client-side ID is public by design.
|
|
10
|
+
*
|
|
11
|
+
* When NEXT_PUBLIC_LD_CLIENT_ID is unset (local dev / offline), LaunchDarkly is
|
|
12
|
+
* not initialized and children render without a provider — `useFlags()` then
|
|
13
|
+
* returns no flags, so the chatbot defaults to hidden.
|
|
14
|
+
*/
|
|
15
|
+
export function ChatbotFlagProvider({
|
|
16
|
+
children,
|
|
17
|
+
}: {
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
}) {
|
|
20
|
+
const clientSideID = process.env.NEXT_PUBLIC_LD_CLIENT_ID;
|
|
21
|
+
const { user } = useP1Auth();
|
|
22
|
+
|
|
23
|
+
if (!clientSideID) {
|
|
24
|
+
return <>{children}</>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// LaunchDarkly evaluates the flag for the context present at mount and does not
|
|
28
|
+
// re-identify on context change. This provider mounts inside <P1App> (after
|
|
29
|
+
// auth), so the authenticated user is available here; the anonymous fallback
|
|
30
|
+
// only applies if it ever renders pre-auth.
|
|
31
|
+
//
|
|
32
|
+
// Key on the always-present, stable user id — email is optional on AuthUser, so
|
|
33
|
+
// keying on it would silently drop emailless users into the anonymous branch and
|
|
34
|
+
// lose per-user rollout stickiness. (LaunchDarkly also favors a non-PII key.)
|
|
35
|
+
// Email is kept as a targeting attribute.
|
|
36
|
+
const context = user
|
|
37
|
+
? { kind: "user" as const, key: user.id, email: user.email }
|
|
38
|
+
: { kind: "user" as const, key: "anonymous", anonymous: true };
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<LDProvider
|
|
42
|
+
clientSideID={clientSideID}
|
|
43
|
+
context={context}
|
|
44
|
+
reactOptions={{ useCamelCaseFlagKeys: false }}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</LDProvider>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
export function PantheonMark() {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
className="h-7 w-auto block"
|
|
7
|
+
viewBox="0 0 105 230"
|
|
8
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
9
|
+
aria-hidden="true"
|
|
10
|
+
>
|
|
11
|
+
<polygon fill="#FFDC28" points="17.8,13.4 35.7,56.4 13,56.4 20.5,75.4 66.6,75.4" />
|
|
12
|
+
<polygon fill="#FFDC28" points="78.4,170.1 70.8,151.2 60.3,151.2 38.3,97.9 28.9,97.9 50.8,151.2 24,151.2 73.6,213.2 55.7,170.1" />
|
|
13
|
+
<path fill="#23232D" d="M84.6,94.3c0.6,0,1.9-0.7,1.9-7.3s-1.3-7.3-1.9-7.3H52.8l6,14.6C58.8,94.3,84.6,94.3,84.6,94.3z" />
|
|
14
|
+
<path fill="#23232D" d="M66.1,111.8l21.3,0c0.6,0,1.9-0.7,1.9-7.3s-1.3-7.3-1.9-7.3l-27.4,0L66.1,111.8z" />
|
|
15
|
+
<path fill="#23232D" d="M84.6,132.2H55.9l6,14.6h22.7c0.6,0,1.9-0.7,1.9-7.3S85.1,132.2,84.6,132.2z" />
|
|
16
|
+
<path fill="#23232D" d="M87.4,114.7H48.7l6,14.6h32.7c0.6,0,1.9-0.7,1.9-7.3S88,114.7,87.4,114.7L87.4,114.7z" />
|
|
17
|
+
<path fill="#23232D" d="M31.1,111.9l-6.8-17.6h15.9l7.4,17.6l15.2-0.1L49.5,79.7H16.5c-2.5,0-3.9,0-5.1,3.8c-1.4,4.5-1.5,13.1-1.5,29.7s0.2,25.2,1.5,29.7c1.1,3.8,2.5,3.8,5.1,3.8l29,0l-14.4-35L31.1,111.9L31.1,111.9z" />
|
|
18
|
+
<path fill="#23232D" d="M91.7,143h-1.2v-0.8h3.4v0.8h-1.2v3.5h-1L91.7,143L91.7,143z M96.3,146.5l-1.1-3.3v3.3h-0.9v-4.3h1.3l1.1,3.3l1.1-3.3H99v4.3h-0.9v-3.3l-1,3.3H96.3L96.3,146.5z" />
|
|
19
|
+
</svg>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function P1Lockup() {
|
|
24
|
+
return (
|
|
25
|
+
<div className="inline-flex items-center gap-2.5 mb-6 px-2 -mx-2 rounded-full">
|
|
26
|
+
<PantheonMark />
|
|
27
|
+
<span className="w-px h-5 bg-gray-200" />
|
|
28
|
+
<span className="font-['Inter_Tight','Inter',system-ui,sans-serif] font-semibold text-[19px] tracking-[0.01em] text-[#1a1a2e] leading-none">
|
|
29
|
+
P1
|
|
30
|
+
</span>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { P1Lockup } from "../p1-lockup";
|
|
5
|
+
|
|
6
|
+
export interface WelcomeBlockRenderProps {
|
|
7
|
+
heading?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
ctaLabel?: string;
|
|
10
|
+
ctaHref?: string;
|
|
11
|
+
footnote?: string;
|
|
12
|
+
loggedInHeading?: string;
|
|
13
|
+
loggedInDescription?: string;
|
|
14
|
+
loggedInCtaLabel?: string;
|
|
15
|
+
loggedInCtaHref?: string;
|
|
16
|
+
loggedInSecondaryLabel?: string;
|
|
17
|
+
loggedInFootnote?: string;
|
|
18
|
+
showLogo?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const LOGGED_IN_DEFAULTS = {
|
|
22
|
+
heading: "Welcome to your new Pantheon P1 Site.",
|
|
23
|
+
description:
|
|
24
|
+
"You just created this new site from Pantheon P1 starter kit, congrats! Start editing this page or visit the P1 dashboard to manage your site.",
|
|
25
|
+
ctaLabel: "Edit this page with P1 Visual Editor",
|
|
26
|
+
ctaHref: "/p1",
|
|
27
|
+
secondaryLabel: "Go to P1 Dashboard",
|
|
28
|
+
secondaryHref: process.env.NEXT_PUBLIC_P1_ADMIN_DASHBOARD_URL || "https://content.pantheon.io",
|
|
29
|
+
footnote:
|
|
30
|
+
"Visit [P1 documentation](https://docs.pantheon.io) for more information.",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const BTN_PRIMARY =
|
|
34
|
+
"inline-flex items-center justify-center h-12 px-6 gap-2 rounded-full border border-[#1a1a2e] bg-[#1a1a2e] text-white font-['Inter',system-ui,sans-serif] text-lg font-medium leading-none whitespace-nowrap cursor-pointer transition-colors duration-200 hover:bg-[#2d2d44] hover:border-[#2d2d44] focus-visible:outline focus-visible:outline-1 focus-visible:outline-blue-600 focus-visible:outline-offset-1 disabled:opacity-40 disabled:cursor-not-allowed";
|
|
35
|
+
|
|
36
|
+
const BTN_SECONDARY =
|
|
37
|
+
"inline-flex items-center justify-center h-12 px-6 gap-2 rounded-full border border-[#d0d0d8] bg-transparent text-[#1a1a2e] font-['Inter',system-ui,sans-serif] text-lg font-medium leading-none whitespace-nowrap cursor-pointer transition-colors duration-200 hover:bg-[rgba(26,26,46,0.06)] focus-visible:outline focus-visible:outline-1 focus-visible:outline-blue-600 focus-visible:outline-offset-1";
|
|
38
|
+
|
|
39
|
+
export function WelcomeBlockRender(props: WelcomeBlockRenderProps) {
|
|
40
|
+
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setIsLoggedIn(!!localStorage.getItem("p1_logged_in"));
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
const activeHeading = isLoggedIn
|
|
47
|
+
? (props.loggedInHeading || LOGGED_IN_DEFAULTS.heading)
|
|
48
|
+
: props.heading;
|
|
49
|
+
const activeDescription = isLoggedIn
|
|
50
|
+
? (props.loggedInDescription || LOGGED_IN_DEFAULTS.description)
|
|
51
|
+
: props.description;
|
|
52
|
+
const activeCtaLabel = isLoggedIn
|
|
53
|
+
? (props.loggedInCtaLabel || LOGGED_IN_DEFAULTS.ctaLabel)
|
|
54
|
+
: props.ctaLabel;
|
|
55
|
+
const activeCtaHref = isLoggedIn
|
|
56
|
+
? (props.loggedInCtaHref || LOGGED_IN_DEFAULTS.ctaHref)
|
|
57
|
+
: props.ctaHref;
|
|
58
|
+
const activeFootnote = isLoggedIn
|
|
59
|
+
? (props.loggedInFootnote || LOGGED_IN_DEFAULTS.footnote)
|
|
60
|
+
: props.footnote;
|
|
61
|
+
const secondaryLabel = isLoggedIn
|
|
62
|
+
? (props.loggedInSecondaryLabel || LOGGED_IN_DEFAULTS.secondaryLabel)
|
|
63
|
+
: null;
|
|
64
|
+
|
|
65
|
+
const footnoteHtml = (activeFootnote ?? "").replace(
|
|
66
|
+
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
67
|
+
'<a href="$2" target="_blank" rel="noopener noreferrer" class="text-blue-600 underline">$1</a>',
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="w-full max-w-[620px] mx-auto flex flex-col items-center text-center px-8 py-16 font-['Inter',system-ui,sans-serif] text-[#1a1a2e] min-h-screen justify-center">
|
|
72
|
+
{props.showLogo !== false && <P1Lockup />}
|
|
73
|
+
<h1 className="text-[2.5rem] leading-[1.08] font-semibold m-0 mb-4">
|
|
74
|
+
{activeHeading}
|
|
75
|
+
</h1>
|
|
76
|
+
<p className="text-base leading-6 text-[#5a5a6e] max-w-[54ch] m-0">
|
|
77
|
+
{activeDescription}
|
|
78
|
+
</p>
|
|
79
|
+
<div className="flex gap-3 mt-8 justify-center">
|
|
80
|
+
<button
|
|
81
|
+
className={BTN_PRIMARY}
|
|
82
|
+
onClick={() => {
|
|
83
|
+
if (!isLoggedIn) {
|
|
84
|
+
localStorage.setItem("p1_return_to", window.location.pathname);
|
|
85
|
+
}
|
|
86
|
+
window.location.href = activeCtaHref || "/";
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
{activeCtaLabel}
|
|
90
|
+
</button>
|
|
91
|
+
{secondaryLabel && (
|
|
92
|
+
<button
|
|
93
|
+
className={BTN_SECONDARY}
|
|
94
|
+
onClick={() => {
|
|
95
|
+
window.open(LOGGED_IN_DEFAULTS.secondaryHref, "_blank", "noopener,noreferrer");
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
{secondaryLabel}
|
|
99
|
+
</button>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
{activeFootnote && (
|
|
103
|
+
<p
|
|
104
|
+
className="mt-12 text-sm text-[#5a5a6e]"
|
|
105
|
+
dangerouslySetInnerHTML={{ __html: footnoteHtml }}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { WelcomeBlockRender } from "./welcome-block-render";
|
|
2
|
+
|
|
3
|
+
export const welcomeBlock = {
|
|
4
|
+
label: "P1 Welcome",
|
|
5
|
+
fields: {
|
|
6
|
+
heading: { type: "text" as const, label: "Heading (signed out)" },
|
|
7
|
+
description: { type: "textarea" as const, label: "Description (signed out)" },
|
|
8
|
+
ctaLabel: { type: "text" as const, label: "Primary button label (signed out)" },
|
|
9
|
+
ctaHref: { type: "text" as const, label: "Primary button link (signed out)" },
|
|
10
|
+
footnote: { type: "textarea" as const, label: "Footnote (signed out)" },
|
|
11
|
+
loggedInHeading: { type: "text" as const, label: "Heading (signed in)" },
|
|
12
|
+
loggedInDescription: { type: "textarea" as const, label: "Description (signed in)" },
|
|
13
|
+
loggedInCtaLabel: { type: "text" as const, label: "Primary button label (signed in)" },
|
|
14
|
+
loggedInCtaHref: { type: "text" as const, label: "Primary button link (signed in)" },
|
|
15
|
+
loggedInSecondaryLabel: { type: "text" as const, label: "Secondary button label (signed in)" },
|
|
16
|
+
loggedInFootnote: { type: "textarea" as const, label: "Footnote (signed in)" },
|
|
17
|
+
showLogo: {
|
|
18
|
+
type: "radio" as const,
|
|
19
|
+
label: "Show P1 logo",
|
|
20
|
+
options: [
|
|
21
|
+
{ label: "Yes", value: true },
|
|
22
|
+
{ label: "No", value: false },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultProps: {
|
|
27
|
+
heading: "Welcome to your new Pantheon P1 Site.",
|
|
28
|
+
description:
|
|
29
|
+
"You just created this new site from Pantheon P1 starter kit, congrats! You'll need a Pantheon P1 user account to edit it and create new pages.",
|
|
30
|
+
ctaLabel: "Sign-in to P1",
|
|
31
|
+
ctaHref: "/p1",
|
|
32
|
+
footnote:
|
|
33
|
+
"Visit [P1 documentation](https://docs.pantheon.io) for more information.",
|
|
34
|
+
loggedInHeading: "Welcome to your new Pantheon P1 Site.",
|
|
35
|
+
loggedInDescription:
|
|
36
|
+
"You just created this new site from Pantheon P1 starter kit, congrats! Start editing this page or visit the P1 dashboard to manage your site.",
|
|
37
|
+
loggedInCtaLabel: "Edit this page with P1 Visual Editor",
|
|
38
|
+
loggedInCtaHref: "/p1",
|
|
39
|
+
loggedInSecondaryLabel: "Go to P1 Dashboard",
|
|
40
|
+
loggedInFootnote:
|
|
41
|
+
"Visit [P1 documentation](https://docs.pantheon.io) for more information.",
|
|
42
|
+
showLogo: true,
|
|
43
|
+
},
|
|
44
|
+
render: WelcomeBlockRender,
|
|
45
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** LaunchDarkly flag that gates the AI chatbot. Short-lived — removed once the
|
|
2
|
+
* chatbot ships to everyone in the alpha. */
|
|
3
|
+
export const CHATBOT_FLAG_KEY = "p1-chatbot";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Whether the AI chatbot plugin should be mounted in the editor.
|
|
7
|
+
*
|
|
8
|
+
* Requires both the `p1-chatbot` LaunchDarkly flag to be enabled and an agent
|
|
9
|
+
* URL to be configured. Defaults off when the flag is `undefined` (LD not yet
|
|
10
|
+
* resolved, unset client ID, or offline), so the chatbot stays hidden by default.
|
|
11
|
+
*/
|
|
12
|
+
export function shouldShowChatbot(
|
|
13
|
+
flagEnabled: boolean | undefined,
|
|
14
|
+
agentUrl: string | undefined,
|
|
15
|
+
): boolean {
|
|
16
|
+
return Boolean(flagEnabled && agentUrl);
|
|
17
|
+
}
|
package/template/package.json
CHANGED
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@pantheon-systems/cpub-react-sdk": "^5.2.1",
|
|
14
|
-
"@pantheon-systems/
|
|
15
|
-
"@pantheon-systems/p1-next-sdk": "^0.
|
|
16
|
-
"@pantheon-systems/
|
|
14
|
+
"@pantheon-systems/p1-ai-chat": "^0.1.0",
|
|
15
|
+
"@pantheon-systems/p1-next-sdk": "^0.6.0",
|
|
16
|
+
"@pantheon-systems/pds-toolkit-react": "2.0.0-alpha.12",
|
|
17
|
+
"@pantheon-systems/puck-css": "^0.6.0",
|
|
17
18
|
"@puckeditor/core": "^0.21.1",
|
|
18
19
|
"@tailwindcss/postcss": "^4.2.2",
|
|
19
20
|
"classnames": "^2.5.1",
|
|
21
|
+
"launchdarkly-react-client-sdk": "^3.9.2",
|
|
20
22
|
"next": "^16.2.6",
|
|
21
23
|
"postcss": "^8.5.12",
|
|
22
24
|
"react": "^19.2.5",
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M0 3C0 1.34315 1.34315 0 3 0H29C30.6569 0 32 1.34315 32 3V29C32 30.6569 30.6569 32 29 32H3C1.34315 32 0 30.6569 0 29V3Z" fill="#171717"/>
|
|
3
|
+
<path d="M6 7H16.9111V8.82232H18.7335V17.9112H16.9111V19.7335H9.64464V25.189H6.01139V7H6ZM15.0888 10.6333H9.63325V16.0888H15.0888V10.6333Z" fill="#FFDC28"/>
|
|
4
|
+
<path d="M20.5449 7H26.0005V25.1777H22.3672V10.6333H20.5449V7Z" fill="#FFDC28"/>
|
|
5
|
+
</svg>
|
package/template/puck.config.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import { paragraphBlock } from "./components/puck/paragraph-block";
|
|
|
10
10
|
import { quoteBlock } from "./components/puck/quote-block";
|
|
11
11
|
import { puckRoot } from "./components/puck/root";
|
|
12
12
|
import { spacerBlock } from "./components/puck/spacer-block";
|
|
13
|
+
import { welcomeBlock } from "./components/puck/welcome-block";
|
|
13
14
|
|
|
14
15
|
export const config = {
|
|
15
16
|
categories: {
|
|
@@ -33,6 +34,10 @@ export const config = {
|
|
|
33
34
|
title: "Actions",
|
|
34
35
|
components: ["ButtonBlock"],
|
|
35
36
|
},
|
|
37
|
+
pages: {
|
|
38
|
+
title: "Page Sections",
|
|
39
|
+
components: ["P1WelcomeBlock"],
|
|
40
|
+
},
|
|
36
41
|
},
|
|
37
42
|
root: puckRoot,
|
|
38
43
|
components: {
|
|
@@ -45,6 +50,7 @@ export const config = {
|
|
|
45
50
|
DividerBlock: dividerBlock,
|
|
46
51
|
SpacerBlock: spacerBlock,
|
|
47
52
|
ButtonBlock: buttonBlock,
|
|
53
|
+
P1WelcomeBlock: welcomeBlock,
|
|
48
54
|
},
|
|
49
55
|
} as Config;
|
|
50
56
|
|
|
@@ -1,9 +0,0 @@
|
|
|
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
|
-
}
|