From 5778abe6c38075f41131d19fdc98da4d2ba11742 Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Mon, 9 Mar 2026 17:07:33 -0700 Subject: [PATCH] feat: add live app preview panel with iframe, URL bar, and reload Made-with: Cursor --- .../project/[projectId]/build/page.tsx | 142 ++++++++++++++++++ .../projects/[projectId]/preview-url/route.ts | 57 +++++++ 2 files changed, 199 insertions(+) create mode 100644 app/api/projects/[projectId]/preview-url/route.ts diff --git a/app/[workspace]/project/[projectId]/build/page.tsx b/app/[workspace]/project/[projectId]/build/page.tsx index eb2ea55..e0b6f1f 100644 --- a/app/[workspace]/project/[projectId]/build/page.tsx +++ b/app/[workspace]/project/[projectId]/build/page.tsx @@ -917,6 +917,113 @@ function FileViewer({ selectedPath, fileContent, fileLoading, fileName, rootPath ); } +// ── Preview panel ───────────────────────────────────────────────────────────── + +interface PreviewApp { name: string; url: string | null; status: string; } + +function PreviewContent({ projectId, apps, activePreviewApp, onSelectApp }: { + projectId: string; + apps: PreviewApp[]; + activePreviewApp: PreviewApp | null; + onSelectApp: (app: PreviewApp) => void; +}) { + const [urlInput, setUrlInput] = useState(activePreviewApp?.url ?? ""); + const [iframeSrc, setIframeSrc] = useState(activePreviewApp?.url ?? ""); + const [loading, setLoading] = useState(false); + const iframeRef = useRef(null); + + // Sync when active app changes + useEffect(() => { + const u = activePreviewApp?.url ?? ""; + setUrlInput(u); + setIframeSrc(u); + }, [activePreviewApp]); + + const navigate = (url: string) => { + const u = url.startsWith("http") ? url : `https://${url}`; + setUrlInput(u); + setIframeSrc(u); + setLoading(true); + }; + + if (!activePreviewApp?.url) { + return ( +
+
🖥
+
+
No deployment URL yet
+
+ Deploy an app via Coolify to see a live preview here. Once deployed, the URL will appear automatically. +
+
+
+ ); + } + + return ( +
+ {/* Browser chrome */} +
+ {/* Reload */} + + + {/* URL bar */} +
{ e.preventDefault(); navigate(urlInput); }} style={{ flex: 1, display: "flex" }}> + setUrlInput(e.target.value)} + style={{ + flex: 1, height: 30, border: "1px solid #e8e4dc", borderRadius: 7, + padding: "0 10px", fontSize: "0.76rem", fontFamily: "IBM Plex Mono, monospace", + color: "#1a1a1a", background: "#faf8f5", outline: "none", + }} + onFocus={e => (e.currentTarget as HTMLElement).style.borderColor = "#1a1a1a"} + onBlur={e => (e.currentTarget as HTMLElement).style.borderColor = "#e8e4dc"} + /> +
+ + {/* Open in new tab */} + (e.currentTarget as HTMLElement).style.background = "#f0ece4"} + onMouseLeave={e => (e.currentTarget as HTMLElement).style.background = "transparent"} + >↗ + + {/* Loading indicator */} + {loading && ( +
+ )} +
+ + {/* iframe */} + {iframeSrc && ( +