234 lines
8.1 KiB
TypeScript
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,
|
|
};
|