@pantheon-systems/create-p1-starter-kit 0.4.4 → 0.5.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pantheon-systems/create-p1-starter-kit",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "Scaffold a new P1 starter project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,3 +14,8 @@ 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
@@ -1,5 +1,13 @@
1
1
  # @pantheon-systems/p1-starter
2
2
 
3
+ ## 1.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [0bc7982]
8
+ - @pantheon-systems/puck-css@0.5.0
9
+ - @pantheon-systems/p1-next-sdk@0.5.0
10
+
3
11
  ## 1.0.3
4
12
 
5
13
  ### 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
+ });
@@ -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
- p1BranchId: process.env.NEXT_PUBLIC_CSS_BRANCH_ID,
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://staging.content.pantheon.io/dashboard/sites"
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,6 +8,7 @@ import {
8
8
  createNextConfig,
9
9
  useP1Editor,
10
10
  useP1Plugins,
11
+ useP1Auth,
11
12
  wrapConfigForEditorPreview,
12
13
  P1QueryProvider,
13
14
  editorPathHref,
@@ -19,8 +20,40 @@ import type { ContentRole } from "@pantheon-systems/puck-css";
19
20
  import "@pantheon-systems/puck-css/styles.css";
20
21
  import "@pantheon-systems/puck-css/pds/styles.css";
21
22
 
23
+ import { P1Lockup } from "../../../components/p1-lockup";
22
24
  import config from "../../../puck.config";
23
25
 
26
+ const DEFAULT_PAGE_DATA = {
27
+ root: { props: { title: "New page" } },
28
+ content: [],
29
+ zones: {},
30
+ };
31
+
32
+ const DEFAULT_ROOT_PAGE_DATA = {
33
+ root: { props: { title: "Welcome | P1 site" } },
34
+ content: [
35
+ {
36
+ type: "P1WelcomeBlock",
37
+ props: {
38
+ id: "seed-welcome",
39
+ heading: "Welcome to your new Pantheon P1 Site.",
40
+ 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.",
41
+ ctaLabel: "Sign-in to P1",
42
+ ctaHref: "/p1",
43
+ footnote: "Visit [P1 documentation](https://docs.pantheon.io) for more information.",
44
+ loggedInHeading: "Welcome to your new Pantheon P1 Site.",
45
+ 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.",
46
+ loggedInCtaLabel: "Edit this page with P1 Visual Editor",
47
+ loggedInCtaHref: "/p1",
48
+ loggedInSecondaryLabel: "Go to P1 Dashboard",
49
+ loggedInFootnote: "Visit [P1 documentation](https://docs.pantheon.io) for more information.",
50
+ showLogo: true,
51
+ },
52
+ },
53
+ ],
54
+ zones: {},
55
+ };
56
+
24
57
  let p1Config: ReturnType<typeof createNextConfig> | null = null;
25
58
  let p1ConfigError: string | null = null;
26
59
 
@@ -34,6 +67,41 @@ const editorConfig = wrapConfigForEditorPreview(config);
34
67
 
35
68
  const ROLES: ContentRole[] = ['admin', 'editor', 'junior-editor'];
36
69
 
70
+ function P1SignInPage() {
71
+ const { login, isLoading, error } = useP1Auth();
72
+
73
+ return (
74
+ <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">
75
+ <P1Lockup />
76
+
77
+ <h1 className="text-[2.5rem] leading-[1.08] font-semibold m-0 mb-3" style={{ fontSize: '2.5rem' }}>
78
+ Your Collaborative Website Management Workspace.
79
+ </h1>
80
+ <p className="text-base leading-6 text-[#5a5a6e] max-w-[54ch] m-0">
81
+ Log in to your Pantheon P1 account to edit your P1 powered website.
82
+ If you don&apos;t have yet a Pantheon P1 account, contact us{" "}
83
+ <a href="https://pantheon.io/contact-us" className="text-blue-600 underline">here</a>.
84
+ </p>
85
+
86
+ <div className="flex gap-3 mt-8 justify-center">
87
+ <button
88
+ 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"
89
+ onClick={() => void login()}
90
+ disabled={isLoading}
91
+ >
92
+ {isLoading ? "Signing in..." : "Continue"}
93
+ </button>
94
+ </div>
95
+
96
+ {error && (
97
+ <p className="mt-4 text-sm text-red-600 bg-red-50 px-3 py-2 rounded-md">
98
+ {error}
99
+ </p>
100
+ )}
101
+ </div>
102
+ );
103
+ }
104
+
37
105
  export function EditorClientWrapper({ path }: { path: string }) {
38
106
  const [userRole, setUserRole] = useState<ContentRole>('editor');
39
107
  const lastGoodStateRef = React.useRef<{ puckKey: string; puckProps: any } | null>(null);
@@ -58,11 +126,13 @@ export function EditorClientWrapper({ path }: { path: string }) {
58
126
  <P1NextRouterProvider>
59
127
  <P1App
60
128
  config={{ ...p1Config, userRole }}
61
- loginPageProps={{ title: "P1 Starter", subtitle: "Sign in to edit" }}
129
+ loginFallback={<P1SignInPage />}
62
130
  >
63
131
  <EditorContent path={path} lastGoodStateRef={lastGoodStateRef} />
64
132
  </P1App>
65
- <RoleSwitcher currentRole={userRole} onRoleChange={setUserRole} />
133
+ {process.env.NEXT_PUBLIC_ENABLE_ROLE_SWITCHER === 'true' && (
134
+ <RoleSwitcher currentRole={userRole} onRoleChange={setUserRole} />
135
+ )}
66
136
  </P1NextRouterProvider>
67
137
  </P1QueryProvider>
68
138
  );
@@ -126,8 +196,20 @@ function EditorContent({
126
196
  lastGoodStateRef: React.MutableRefObject<{ puckKey: string; puckProps: any } | null>;
127
197
  }) {
128
198
  const router = useRouter();
199
+ const { getToken } = useP1Auth();
129
200
  const p1Plugins = useP1Plugins(path, config);
130
201
 
202
+ const [redirecting, setRedirecting] = React.useState(false);
203
+
204
+ React.useEffect(() => {
205
+ const returnTo = localStorage.getItem("p1_return_to");
206
+ if (returnTo) {
207
+ localStorage.removeItem("p1_return_to");
208
+ setRedirecting(true);
209
+ router.push(returnTo);
210
+ }
211
+ }, [router]);
212
+
131
213
  const handleDocumentSelect = useCallback(
132
214
  (docPath: string) => {
133
215
  router.push(editorPathHref(docPath));
@@ -135,10 +217,27 @@ function EditorContent({
135
217
  [router],
136
218
  );
137
219
 
220
+ const handleDocumentNotFound = useCallback(
221
+ async (docPath: string, _error: Error) => {
222
+ const initialData = docPath === "/" ? DEFAULT_ROOT_PAGE_DATA : DEFAULT_PAGE_DATA;
223
+ const token = await getToken();
224
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
225
+ if (token) headers["Authorization"] = `Bearer ${token}`;
226
+ const res = await fetch("/p1/api/structure/page", {
227
+ method: "POST",
228
+ headers,
229
+ body: JSON.stringify({ path: docPath, initialData }),
230
+ });
231
+ return res.ok;
232
+ },
233
+ [getToken],
234
+ );
235
+
138
236
  const { loading, error, puckKey, puckProps } = useP1Editor({
139
237
  documentPath: path,
140
238
  puckConfig: editorConfig,
141
239
  additionalPlugins: p1Plugins,
240
+ onDocumentNotFound: handleDocumentNotFound,
142
241
  pluginOptions: {
143
242
  onDocumentSelect: handleDocumentSelect,
144
243
  selectedDocumentPath: path,
@@ -163,6 +262,14 @@ function EditorContent({
163
262
  }
164
263
  }, [loading, error, puckKey, puckProps]);
165
264
 
265
+ if (redirecting) {
266
+ return (
267
+ <div style={{ textAlign: "center", padding: "4rem", fontFamily: "system-ui" }}>
268
+ Redirecting...
269
+ </div>
270
+ );
271
+ }
272
+
166
273
  // Show full loading screen only on first load (no previous state)
167
274
  if (loading && !lastGoodStateRef.current) {
168
275
  return (
@@ -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
- p1BranchId: process.env.NEXT_PUBLIC_CSS_BRANCH_ID,
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: 'login',
6
+ prompt: "select_account",
7
7
  });
8
8
 
9
9
  export const { POST } = handler;
@@ -1,6 +1,5 @@
1
- import Link from "next/link";
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
- p1BranchId: process.env.NEXT_PUBLIC_CSS_BRANCH_ID,
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
- <main className="flex min-h-screen items-center justify-center bg-gray-50">
86
- <div className="mx-auto max-w-lg px-6 py-16 text-center">
87
- <h1 className="text-3xl font-bold tracking-tight text-gray-900">
88
- Welcome to the P1 Starter Kit
89
- </h1>
90
- <p className="mt-4 text-gray-600">
91
- Build and manage pages with the visual editor, or head to the
92
- dashboard to manage your site.
93
- </p>
94
-
95
- <nav className="mt-10 flex flex-col gap-3">
96
- <Link
97
- href="/p1"
98
- className="rounded-lg bg-gray-900 px-5 py-3 text-sm font-medium text-white hover:bg-gray-700"
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 &rarr;
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,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
+ };
@@ -11,9 +11,8 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@pantheon-systems/cpub-react-sdk": "^5.2.1",
14
- "@pantheon-systems/css-client": "^0.4.4",
15
- "@pantheon-systems/p1-next-sdk": "^0.4.4",
16
- "@pantheon-systems/puck-css": "^0.4.4",
14
+ "@pantheon-systems/p1-next-sdk": "^0.5.0",
15
+ "@pantheon-systems/puck-css": "^0.5.0",
17
16
  "@puckeditor/core": "^0.21.1",
18
17
  "@tailwindcss/postcss": "^4.2.2",
19
18
  "classnames": "^2.5.1",
@@ -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
- }