diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx index 91d8d89..775c30c 100644 --- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx @@ -71,7 +71,8 @@ export default function PreviewTab() { ) .sort((a, b) => a.port - b.port); // sort ports ascending - const [selectedPort, setSelectedPort] = useState(3000); + const selectedPort = usePreviewToolbarStore((s) => s.selectedPort); + const setSelectedPort = usePreviewToolbarStore((s) => s.setSelectedPort); // Auto-select logic if selectedPort is not in validPreviews useEffect(() => { @@ -83,7 +84,7 @@ export default function PreviewTab() { validPreviews.find((p) => p.port === 3000) ?? validPreviews[0]; if (fallback) setSelectedPort(fallback.port); } - }, [validPreviews, selectedPort]); + }, [validPreviews, selectedPort, setSelectedPort]); // Derive the currently selected preview const activePreview = validPreviews.find((p) => p.port === selectedPort); @@ -232,27 +233,6 @@ export default function PreviewTab() { return (
- {validPreviews.length > 1 && ( -
- {validPreviews.map((p) => ( - - ))} -
- )}
void; currentPath: string; setCurrentPath: (path: string) => void; + selectedPort: number; + setSelectedPort: (port: number) => void; } export const usePreviewToolbarStore = create((set) => ({ @@ -16,4 +18,6 @@ export const usePreviewToolbarStore = create((set) => ({ triggerRefresh: () => set((state) => ({ refreshKey: state.refreshKey + 1 })), currentPath: "/", setCurrentPath: (path) => set({ currentPath: path }), + selectedPort: 3000, + setSelectedPort: (port) => set({ selectedPort: port }), })); diff --git a/vibn-frontend/components/project/project-icon-rail.tsx b/vibn-frontend/components/project/project-icon-rail.tsx index a3bc4aa..c253fb7 100644 --- a/vibn-frontend/components/project/project-icon-rail.tsx +++ b/vibn-frontend/components/project/project-icon-rail.tsx @@ -1,5 +1,6 @@ "use client"; +import { useEffect, useState } from "react"; import Link from "next/link"; import { usePathname, useParams } from "next/navigation"; import { @@ -8,6 +9,7 @@ import { Smartphone, RotateCw, ExternalLink, + ChevronDown, } from "lucide-react"; import { usePreviewToolbarStore } from "./preview-toolbar/preview-toolbar-state"; import { useAnatomy } from "@/components/project/use-anatomy"; @@ -94,13 +96,32 @@ function PreviewDeviceToggles() { const triggerRefresh = usePreviewToolbarStore((s) => s.triggerRefresh); const currentPath = usePreviewToolbarStore((s) => s.currentPath); const setCurrentPath = usePreviewToolbarStore((s) => s.setCurrentPath); + const selectedPort = usePreviewToolbarStore((s) => s.selectedPort); + const setSelectedPort = usePreviewToolbarStore((s) => s.setSelectedPort); const params = useParams(); const projectId = params?.projectId as string; const { anatomy } = useAnatomy(projectId); + const [now, setNow] = useState(() => Date.now()); + useEffect(() => { + const id = setInterval(() => setNow(Date.now()), 10000); + return () => clearInterval(id); + }, []); + const previews = anatomy?.hosting.previews ?? []; - const running = previews.find((p) => p.state === "running"); + const validPreviews = previews + .filter( + (p) => + p.state === "running" || + (p.state === "starting" && + now - new Date(p.startedAt).getTime() < 15 * 60 * 1000), + ) + .sort((a, b) => a.port - b.port); // sort ports ascending + + const activePreview = validPreviews.find((p) => p.port === selectedPort); + const running = + activePreview?.state === "running" ? activePreview : undefined; const displayUrl = running?.url ?? ""; return ( @@ -159,6 +180,63 @@ function PreviewDeviceToggles() { }} /> + {/* Server Selection Dropdown */} +
+ + +
+ +
+ / - - setCurrentPath("/" + e.target.value.replace(/^\//, "")) - } - placeholder="path (e.g. dashboard)" +
{ - if (e.key === "Enter") { - triggerRefresh(); // force reload iframe + > + + setCurrentPath("/" + e.target.value.replace(/^\//, "")) } - }} - /> + placeholder="path (e.g. dashboard)" + style={{ + background: "transparent", + border: "none", + outline: "none", + width: "100%", + color: "#18181b", + fontSize: "0.75rem", + textOverflow: "ellipsis", + fontFamily: "var(--font-mono), monospace", + paddingRight: 16, + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + triggerRefresh(); // force reload iframe + } + }} + /> + + + +