Files
vibn-frontend/app/[workspace]/project/[projectId]/deployment/page.tsx
Mark Henderson d60d300a64 Complete Stackless parity: Activity, Deploy, Settings, header desc
- Add project description line to project header (from productVision)
- Sidebar: add Activity nav item (Projects / Activity / Settings)
- New Activity page: timeline feed with type filters (Atlas/Builds/Deploys/You)
- New Activity layout using VIBNSidebar
- Rewrite Deploy tab: Project URLs, Custom Domain, Env Vars, Deploy History
  — fully Stackless style, real data from project API, no more MOCK_PROJECT
- Rewrite Project Settings tab: remove all Firebase refs (db, auth, Firestore)
  — General (name/description), Repo link, Collaborators, Export JSON/PDF,
  — Danger Zone with double-confirm delete
  — uses /api/projects/[id] PATCH for saves

Made-with: Cursor
2026-03-02 16:33:09 -08:00

205 lines
9.6 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { toast } from "sonner";
interface Project {
id: string;
productName: string;
status?: string;
giteaRepoUrl?: string;
giteaRepo?: string;
theiaWorkspaceUrl?: string;
coolifyDeployUrl?: string;
customDomain?: string;
prd?: string;
}
function SectionLabel({ children }: { children: React.ReactNode }) {
return (
<div style={{
fontSize: "0.6rem", fontWeight: 600, color: "#a09a90",
letterSpacing: "0.1em", textTransform: "uppercase", marginBottom: 12,
}}>
{children}
</div>
);
}
function InfoCard({ children, style = {} }: { children: React.ReactNode; style?: React.CSSProperties }) {
return (
<div style={{
background: "#fff", border: "1px solid #e8e4dc", borderRadius: 10,
boxShadow: "0 1px 2px #1a1a1a05", marginBottom: 12, ...style,
}}>
{children}
</div>
);
}
export default function DeploymentPage() {
const params = useParams();
const projectId = params.projectId as string;
const [project, setProject] = useState<Project | null>(null);
const [loading, setLoading] = useState(true);
const [customDomainInput, setCustomDomainInput] = useState("");
const [connecting, setConnecting] = useState(false);
useEffect(() => {
fetch(`/api/projects/${projectId}`)
.then((r) => r.json())
.then((d) => setProject(d.project))
.catch(() => {})
.finally(() => setLoading(false));
}, [projectId]);
const handleConnectDomain = async () => {
if (!customDomainInput.trim()) return;
setConnecting(true);
await new Promise((r) => setTimeout(r, 800));
toast.info("Domain connection coming soon — we'll hook this to Coolify.");
setConnecting(false);
};
if (loading) {
return (
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%", fontFamily: "Outfit, sans-serif", color: "#a09a90" }}>
Loading
</div>
);
}
const hasDeploy = Boolean(project?.coolifyDeployUrl || project?.theiaWorkspaceUrl);
const hasRepo = Boolean(project?.giteaRepoUrl);
const hasPRD = Boolean(project?.prd);
return (
<div
className="vibn-enter"
style={{ padding: "28px 32px", overflow: "auto", fontFamily: "Outfit, sans-serif" }}
>
<div style={{ maxWidth: 560 }}>
<h3 style={{
fontFamily: "Newsreader, serif", fontSize: "1.2rem",
fontWeight: 400, color: "#1a1a1a", marginBottom: 4,
}}>
Deployment
</h3>
<p style={{ fontSize: "0.8rem", color: "#a09a90", marginBottom: 24 }}>
Links, environments, and hosting for {project?.productName ?? "this project"}
</p>
{/* Project URLs */}
<InfoCard style={{ padding: "22px" }}>
<SectionLabel>Project URLs</SectionLabel>
{hasDeploy ? (
<>
{project?.coolifyDeployUrl && (
<div style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 0", borderBottom: "1px solid #f0ece4" }}>
<div style={{ width: 28, height: 28, borderRadius: 6, background: "#f6f4f0", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "0.7rem", color: "#8a8478" }}></div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: "0.68rem", color: "#b5b0a6", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.04em" }}>Staging</div>
<div style={{ fontSize: "0.84rem", fontFamily: "IBM Plex Mono, monospace", color: "#3d5afe", fontWeight: 500 }}>{project.coolifyDeployUrl}</div>
</div>
<a href={project.coolifyDeployUrl} target="_blank" rel="noopener noreferrer"
style={{ padding: "5px 12px", borderRadius: 7, border: "1px solid #e0dcd4", background: "#fff", color: "#1a1a1a", fontSize: "0.7rem", fontWeight: 600, textDecoration: "none", fontFamily: "Outfit, sans-serif" }}>
Open
</a>
</div>
)}
{project?.customDomain && (
<div style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 0", borderBottom: "1px solid #f0ece4" }}>
<div style={{ width: 28, height: 28, borderRadius: 6, background: "#2e7d3210", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "0.7rem", color: "#2e7d32" }}></div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: "0.68rem", color: "#b5b0a6", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.04em" }}>Production</div>
<div style={{ fontSize: "0.84rem", fontFamily: "IBM Plex Mono, monospace", color: "#2e7d32", fontWeight: 500 }}>{project.customDomain}</div>
</div>
<span style={{ display: "inline-flex", alignItems: "center", padding: "3px 9px", borderRadius: 4, fontSize: "0.68rem", fontWeight: 600, color: "#2e7d32", background: "#2e7d3210" }}>SSL Active</span>
<a href={`https://${project.customDomain}`} target="_blank" rel="noopener noreferrer"
style={{ padding: "5px 12px", borderRadius: 7, border: "1px solid #e0dcd4", background: "#fff", color: "#1a1a1a", fontSize: "0.7rem", fontWeight: 600, textDecoration: "none", fontFamily: "Outfit, sans-serif" }}>
Open
</a>
</div>
)}
{project?.giteaRepoUrl && (
<div style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 0" }}>
<div style={{ width: 28, height: 28, borderRadius: 6, background: "#f6f4f0", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "0.7rem", color: "#8a8478" }}></div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: "0.68rem", color: "#b5b0a6", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.04em" }}>Build repo</div>
<div style={{ fontSize: "0.84rem", fontFamily: "IBM Plex Mono, monospace", color: "#6b6560", fontWeight: 500 }}>{project.giteaRepo}</div>
</div>
<a href={project.giteaRepoUrl} target="_blank" rel="noopener noreferrer"
style={{ padding: "5px 12px", borderRadius: 7, border: "1px solid #e0dcd4", background: "#fff", color: "#1a1a1a", fontSize: "0.7rem", fontWeight: 600, textDecoration: "none", fontFamily: "Outfit, sans-serif" }}>
View
</a>
</div>
)}
</>
) : (
<div style={{ padding: "18px 0", textAlign: "center" }}>
<p style={{ fontSize: "0.82rem", color: "#a09a90", marginBottom: 12 }}>
{!hasPRD
? "Complete your PRD with Atlas first, then build and deploy."
: !hasRepo
? "No repository yet — the Architect agent will scaffold one from your PRD."
: "No deployment yet — kick off a build to get a live URL."}
</p>
</div>
)}
</InfoCard>
{/* Custom domain */}
{hasDeploy && !project?.customDomain && (
<InfoCard style={{ padding: "22px" }}>
<SectionLabel>Custom Domain</SectionLabel>
<p style={{ fontSize: "0.82rem", color: "#6b6560", lineHeight: 1.6, marginBottom: 14 }}>
Point your own domain to this project. SSL certificates are handled automatically.
</p>
<div style={{ display: "flex", gap: 8 }}>
<input
placeholder="app.yourdomain.com"
value={customDomainInput}
onChange={(e) => setCustomDomainInput(e.target.value)}
style={{ flex: 1, padding: "9px 13px", borderRadius: 7, border: "1px solid #e0dcd4", background: "#faf8f5", fontSize: "0.84rem", fontFamily: "IBM Plex Mono, monospace", color: "#1a1a1a" }}
/>
<button
onClick={handleConnectDomain}
disabled={connecting}
style={{ padding: "9px 18px", borderRadius: 7, border: "none", background: "#1a1a1a", color: "#fff", fontSize: "0.78rem", fontWeight: 600, cursor: "pointer", fontFamily: "Outfit, sans-serif", opacity: connecting ? 0.6 : 1 }}
>
{connecting ? "Connecting…" : "Connect"}
</button>
</div>
</InfoCard>
)}
{/* Environment variables */}
<InfoCard style={{ padding: "22px" }}>
<SectionLabel>Environment Variables</SectionLabel>
{hasDeploy ? (
<p style={{ fontSize: "0.82rem", color: "#a09a90", padding: "10px 0" }}>
Manage environment variables in Coolify for your deployed services.
{project?.coolifyDeployUrl && (
<> <a href="http://34.19.250.135:8000" target="_blank" rel="noopener noreferrer" style={{ color: "#3d5afe", textDecoration: "none" }}>Open Coolify </a></>
)}
</p>
) : (
<p style={{ fontSize: "0.82rem", color: "#a09a90", padding: "10px 0" }}>Available after first build completes.</p>
)}
</InfoCard>
{/* Deploy history */}
<InfoCard style={{ padding: "22px" }}>
<SectionLabel>Deploy History</SectionLabel>
<p style={{ fontSize: "0.82rem", color: "#a09a90", padding: "10px 0" }}>
{project?.status === "live"
? "Deploy history will appear here."
: "No deploys yet."}
</p>
</InfoCard>
</div>
</div>
);
}