Files
vibn-agent-runner/vibn-frontend/app/[workspace]/project/[projectId]/(home)/settings/page.tsx

234 lines
8.1 KiB
TypeScript

"use client";
import { useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { Settings, Trash2, AlertTriangle, Loader2, ArrowLeft } from "lucide-react";
import { WorkspaceKeysPanel } from "@/components/workspace/WorkspaceKeysPanel";
import Link from "next/link";
/**
* Project settings page.
* Accessible via the gear icon in the project header.
*
* Sections:
* - General (name, description — future)
* - Danger zone: delete project
*/
export default function ProjectSettingsPage() {
const params = useParams();
const router = useRouter();
const projectId = params.projectId as string;
const workspace = params.workspace as string;
const [deletePhase, setDeletePhase] = useState<"idle" | "confirm" | "deleting" | "done">("idle");
const [confirmInput, setConfirmInput] = useState("");
const [deleteError, setDeleteError] = useState<string | null>(null);
const projectBackUrl = `/${workspace}/project/${projectId}/plan`;
const handleDelete = async () => {
if (deletePhase === "idle") {
setDeletePhase("confirm");
return;
}
if (deletePhase !== "confirm") return;
if (confirmInput.toLowerCase() !== "delete") return;
setDeletePhase("deleting");
setDeleteError(null);
try {
const r = await fetch("/api/projects/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ projectId }),
});
const d = await r.json();
if (!r.ok) throw new Error(d.error || "Delete failed");
setDeletePhase("done");
setTimeout(() => router.push(`/${workspace}/projects`), 1500);
} catch (e) {
setDeleteError(e instanceof Error ? e.message : String(e));
setDeletePhase("confirm");
}
};
return (
<div style={pageWrap}>
{/* Back link */}
<Link href={projectBackUrl} style={backLink}>
<ArrowLeft size={14} /> Back to project
</Link>
<h1 style={pageTitle}>
<Settings size={18} /> Project settings
</h1>
<div style={{ marginBottom: 40 }}><WorkspaceKeysPanel workspaceSlug={workspace} /></div>
{/* ── Danger zone ── */}
<section style={dangerSection}>
<h2 style={sectionTitle}>
<AlertTriangle size={15} style={{ color: DANGER }} />
Danger zone
</h2>
<div style={dangerCard}>
<div style={dangerCardBody}>
<div>
<div style={dangerItemTitle}>Delete this project</div>
<div style={dangerItemDesc}>
Removes all project data from Vibn. Coolify services and databases
are <strong>not</strong> automatically stopped use the chat to clean those
up first, or remove them from Coolify directly.
</div>
</div>
{deletePhase === "idle" && (
<button onClick={handleDelete} style={dangerBtn}>
<Trash2 size={13} /> Delete project
</button>
)}
{deletePhase === "confirm" && (
<div style={confirmBox}>
<div style={{ fontSize: "0.82rem", color: DANGER, fontWeight: 600, marginBottom: 8 }}>
Type <strong>delete</strong> to confirm
</div>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<input
autoFocus
value={confirmInput}
onChange={e => setConfirmInput(e.target.value)}
onKeyDown={e => e.key === "Enter" && confirmInput.toLowerCase() === "delete" && handleDelete()}
placeholder="delete"
style={confirmInput_}
/>
<button
onClick={handleDelete}
disabled={confirmInput.toLowerCase() !== "delete"}
style={{
...dangerBtn,
opacity: confirmInput.toLowerCase() !== "delete" ? 0.4 : 1,
}}
>
<Trash2 size={13} /> Confirm delete
</button>
<button
onClick={() => { setDeletePhase("idle"); setConfirmInput(""); setDeleteError(null); }}
style={cancelBtn}
>
Cancel
</button>
</div>
{deleteError && (
<div style={{ marginTop: 8, fontSize: "0.8rem", color: DANGER }}>{deleteError}</div>
)}
</div>
)}
{deletePhase === "deleting" && (
<button style={{ ...dangerBtn, opacity: 0.6 }} disabled>
<Loader2 size={13} className="animate-spin" /> Deleting
</button>
)}
{deletePhase === "done" && (
<div style={{ fontSize: "0.85rem", color: "#2e7d32", fontWeight: 600 }}>
Project deleted. Redirecting
</div>
)}
</div>
</div>
</section>
</div>
);
}
// ──────────────────────────────────────────────────
// Tokens
// ──────────────────────────────────────────────────
const DANGER = "#c5392b";
const INK = {
ink: "#1a1a1a",
mid: "#5f5e5a",
muted: "#a09a90",
border: "#e8e4dc",
borderSoft: "#efebe1",
fontSans: '"Outfit", "Inter", ui-sans-serif, sans-serif',
} as const;
// ──────────────────────────────────────────────────
// Styles
// ──────────────────────────────────────────────────
const pageWrap: React.CSSProperties = {
padding: "28px 48px 64px",
fontFamily: INK.fontSans,
color: INK.ink,
maxWidth: 720,
};
const backLink: React.CSSProperties = {
display: "inline-flex", alignItems: "center", gap: 6,
fontSize: "0.8rem", color: INK.mid, textDecoration: "none",
marginBottom: 24,
};
const pageTitle: React.CSSProperties = {
display: "flex", alignItems: "center", gap: 10,
fontSize: "1.25rem", fontWeight: 700, color: INK.ink,
marginBottom: 36, marginTop: 0,
};
const dangerSection: React.CSSProperties = { marginTop: 32 };
const sectionTitle: React.CSSProperties = {
display: "flex", alignItems: "center", gap: 8,
fontSize: "0.72rem", fontWeight: 700, letterSpacing: "0.12em",
textTransform: "uppercase", color: INK.muted,
marginBottom: 12,
};
const dangerCard: React.CSSProperties = {
border: `1px solid #f0cac5`,
borderRadius: 10,
background: "#fffaf9",
};
const dangerCardBody: React.CSSProperties = {
padding: "18px 20px",
display: "flex",
alignItems: "flex-start",
justifyContent: "space-between",
gap: 24,
flexWrap: "wrap",
};
const dangerItemTitle: React.CSSProperties = {
fontWeight: 600, fontSize: "0.9rem", color: INK.ink, marginBottom: 4,
};
const dangerItemDesc: React.CSSProperties = {
fontSize: "0.8rem", color: INK.mid, lineHeight: 1.55, maxWidth: 380,
};
const dangerBtn: React.CSSProperties = {
display: "inline-flex", alignItems: "center", gap: 6,
padding: "7px 14px", border: `1px solid ${DANGER}`,
borderRadius: 6, background: "#fff", cursor: "pointer",
font: "inherit", fontSize: "0.8rem", fontWeight: 600, color: DANGER,
whiteSpace: "nowrap", flexShrink: 0,
};
const cancelBtn: React.CSSProperties = {
display: "inline-flex", alignItems: "center",
padding: "7px 12px", border: `1px solid ${INK.border}`,
borderRadius: 6, background: "#fff", cursor: "pointer",
font: "inherit", fontSize: "0.8rem", color: INK.mid,
whiteSpace: "nowrap",
};
const confirmBox: React.CSSProperties = { display: "flex", flexDirection: "column" };
const confirmInput_: React.CSSProperties = {
padding: "7px 10px",
border: `1px solid ${DANGER}`,
borderRadius: 6,
font: "inherit",
fontSize: "0.85rem",
outline: "none",
width: 100,
};