diff --git a/app/page.tsx b/app/page.tsx index ab99a5b..3d1b436 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useMemo, useRef, useState } from "react"; +import { createPortal } from "react-dom"; import dynamic from "next/dynamic"; import { CHAIN_OPTIONS, type EnvKey } from "@/lib/config"; @@ -53,6 +54,27 @@ const INITIAL_ENV: Record = { NEXT_PUBLIC_SITE_URL: "", }; +// Walkthrough shown in the "Connect Vercel" dialog. Drop the screenshots into +// public/vercel-guide/ with these exact names. The red highlight is already +// baked into these screenshots, so no `highlight` overlay is set here. +const VERCEL_GUIDE_STEPS: GuideStep[] = [ + { + image: "/vercel-guide/step-1-add-integration.png", + alt: "Vercel integration page with the Add Integration button highlighted", + caption: 'Click "Add Integration"', + }, + { + image: "/vercel-guide/step-2-select-team-install.png", + alt: "Vercel install dialog with the team selector highlighted", + caption: 'Select your Vercel team, then click "Install"', + }, + { + image: "/vercel-guide/step-3-configure.png", + alt: "Vercel installation overview with the Configure button highlighted", + caption: 'Click "Configure" to come back and finish setup', + }, +]; + // Dev-only screen preview (?screen=review): seeds the state a screen needs // so it can be styled without going through OAuth. No flows are simulated. const previewTarget = @@ -288,6 +310,18 @@ export default function Home() { useEffect(() => { if (previewTarget || screen !== "building" || !deployment) return; + // Tolerate transient failures, but give up after ~30s of consecutive + // ones (expired session, deleted deployment) instead of spinning forever. + let failures = 0; + const stall = () => { + failures += 1; + if (failures >= 10) { + setError( + "Lost track of the build — open the build logs on Vercel to follow it.", + ); + setScreen("error"); + } + }; const poll = async () => { try { const res = await fetch("/api/status", { @@ -295,16 +329,23 @@ export default function Home() { body: JSON.stringify({ deploymentId: deployment.uid }), }); const data = await res.json(); - if (data.deployment) { - setDeployment(data.deployment); - if (data.deployment.readyState === "READY") setScreen("done"); - if (data.deployment.readyState === "ERROR") { - setError("Build failed. Check the inspector for details."); - setScreen("error"); - } + if (!res.ok || !data.deployment) { + stall(); + return; + } + failures = 0; + setDeployment(data.deployment); + if (data.deployment.readyState === "READY") setScreen("done"); + if (data.deployment.readyState === "ERROR") { + setError("Build failed. Check the inspector for details."); + setScreen("error"); + } + if (data.deployment.readyState === "CANCELED") { + setError("Build was canceled. Check the inspector for details."); + setScreen("error"); } } catch { - // transient network error — keep polling + stall(); } }; pollRef.current = setInterval(poll, 3000); @@ -365,7 +406,8 @@ export default function Home() { installHref="/api/vercel/install" questionNumber={1} question="First, connect Vercel" - hint="We deploy your site to your own Vercel account." + hint="Connection 1 of 2 — you'll connect GitHub next. We deploy your site to your own Vercel account." + steps={VERCEL_GUIDE_STEPS} /> )} {screen === "github" && ( @@ -375,7 +417,8 @@ export default function Home() { installHref="/api/github/install" questionNumber={me?.vercel.connected ? 2 : 1} question="Now, connect GitHub" - hint="So we can fork the template into your account." + hint="Connection 2 of 2 — the last one. So we can fork the template into your account." + confirmHint={'On GitHub, click the green "Authorize" button to continue.'} /> )} {screen === "vercel-bridge" && ( @@ -653,8 +696,9 @@ function WelcomeScreen({ > Builder DAO {" "} - front-ends. Connect your accounts, point us at your DAO, and we ship - a fully-configured site to your Vercel — no code, no copy-paste. + front-ends. Connect your Vercel and GitHub accounts, point us at your + DAO, and we ship a fully-configured site to your Vercel — no code, no + copy-paste.

@@ -697,6 +741,61 @@ function Step({ ); } +// A single step shown inside the connect dialog. `image` is a path under +// /public; `highlight` draws a red box over the screenshot (percent values so +// it scales with the image). Tweak the highlight numbers after dropping in the +// real screenshots — no need to re-edit the image itself. +type GuideStep = { + image: string; + alt: string; + caption: string; + highlight?: { top: string; left: string; width: string; height: string }; +}; + +function GuideImage({ + src, + alt, + highlight, +}: { + src: string; + alt: string; + highlight?: GuideStep["highlight"]; +}) { + const [failed, setFailed] = useState(false); + return ( +
+ {failed ? ( +
+ Screenshot pending + + {src} + +
+ ) : ( + // eslint-disable-next-line @next/next/no-img-element + {alt} setFailed(true)} + className="h-full w-full object-contain" + /> + )} + {highlight && ( + + )} +
+ ); +} + function ConnectScreen({ name, icon, @@ -704,6 +803,8 @@ function ConnectScreen({ questionNumber, question, hint, + steps, + confirmHint, }: { name: string; icon: React.ReactNode; @@ -711,13 +812,40 @@ function ConnectScreen({ questionNumber: number; question: string; hint: string; + steps?: GuideStep[]; + confirmHint?: string; }) { + const [confirming, setConfirming] = useState(false); + const [step, setStep] = useState(0); + const hasGuide = !!steps && steps.length > 0; + const lastStep = hasGuide ? steps!.length - 1 : 0; + const current = hasGuide ? steps![Math.min(step, lastStep)] : undefined; + + const openDialog = () => { + setStep(0); + setConfirming(true); + }; + + // While the dialog is open: Escape closes it, arrows step through the guide. + useEffect(() => { + if (!confirming) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") setConfirming(false); + if (!hasGuide) return; + if (e.key === "ArrowRight") setStep((s) => Math.min(s + 1, lastStep)); + if (e.key === "ArrowLeft") setStep((s) => Math.max(s - 1, 0)); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [confirming, hasGuide, lastStep]); + return (
-
@@ -733,7 +861,133 @@ function ConnectScreen({ - + + + {confirming && + typeof document !== "undefined" && + createPortal( +
+
setConfirming(false)} + /> +
+
+
+ {icon} +
+
+
+ {hasGuide + ? `How to connect ${name}` + : `You're leaving for ${name}`} +
+
+ {hasGuide + ? `Follow these steps once ${name} opens.` + : "You'll come back here automatically afterwards."} +
+
+
+ +
+ {hasGuide && current ? ( +
+
+ + {step + 1} + + + {current.caption} + +
+ + + +
+ + +
+ {steps!.map((s, i) => ( +
+ + +
+
+ ) : ( +
+

+ We'll open {name} in this window so you can authorize + the connection. After you approve, you'll come back + here automatically to continue setup. +

+ {confirmHint && ( +

+ {confirmHint} +

+ )} +
+ )} +
+ +
+ + + Continue to {name} → + +
+
+
, + document.body, + )}
); } @@ -1306,8 +1560,8 @@ function QuestionHeader({ return (
{number !== undefined && ( -
- {number} → +
+ Step {number}
)}

diff --git a/public/vercel-guide/README.md b/public/vercel-guide/README.md new file mode 100644 index 0000000..2f61347 --- /dev/null +++ b/public/vercel-guide/README.md @@ -0,0 +1,23 @@ +# Vercel connect walkthrough screenshots + +These images show up inside the **Connect Vercel** dialog (the popup that +appears when you click the "Connect Vercel" card). + +Save the three screenshots with these exact names: + +| File | What it shows | +| --------------------------------- | ---------------------------------------------------------------- | +| `step-1-add-integration.png` | The integration page with the **Add Integration** button | +| `step-2-select-team-install.png` | The install dialog with the **team selector** / Install button | +| `step-3-configure.png` | The installation overview with the **Configure** button | + +Notes: + +- These screenshots already have the red highlight box drawn in, so the code + does **not** add an overlay (`VERCEL_GUIDE_STEPS` in `app/page.tsx` has no + `highlight` field). If you swap in clean screenshots later, you can add a + `highlight` with percent coordinates to draw the box in code instead. +- Images are shown at a 16:10 ratio with `object-cover`. Crop roughly to that + shape so nothing important gets clipped. +- Until a file exists, the dialog shows a "Screenshot pending" placeholder in + its slot — the rest of the flow still works. diff --git a/public/vercel-guide/step-1-add-integration.png b/public/vercel-guide/step-1-add-integration.png new file mode 100644 index 0000000..bd924fc Binary files /dev/null and b/public/vercel-guide/step-1-add-integration.png differ diff --git a/public/vercel-guide/step-2-select-team-install.png b/public/vercel-guide/step-2-select-team-install.png new file mode 100644 index 0000000..0abc216 Binary files /dev/null and b/public/vercel-guide/step-2-select-team-install.png differ diff --git a/public/vercel-guide/step-3-configure.png b/public/vercel-guide/step-3-configure.png new file mode 100644 index 0000000..fe5f143 Binary files /dev/null and b/public/vercel-guide/step-3-configure.png differ