import { Save } from "lucide-react"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { Breadcrumbs, Button, ButtonLink, Card, ErrorBanner, Input, PageHeader } from "@/client/components/ui"; import { buildProjectSlug, formatApiError, getApiClient, inferRepositoryProvider } from "@/client/lib"; import { useToast } from "@/client/toast"; import { DEFAULT_DISPATCH_MODE, type DispatchMode } from "@/contracts"; interface ProjectFormState { name: string; projectSlug: string; repoUrl: string; defaultBranch: string; configPath: string; repoToken: string; dispatchMode: DispatchMode; } const initialFormState: ProjectFormState = { name: "", projectSlug: "", repoUrl: "", defaultBranch: "main", configPath: ".anvil.yml", repoToken: "", dispatchMode: DEFAULT_DISPATCH_MODE, }; export const CreateProjectPage = () => { const navigate = useNavigate(); const { pushToast } = useToast(); const [form, setForm] = useState(initialFormState); const [error, setError] = useState(null); const [submitting, setSubmitting] = useState(false); const [slugTouched, setSlugTouched] = useState(false); const updateField = (field: keyof ProjectFormState, value: ProjectFormState[keyof ProjectFormState]) => { setForm((current) => { const next = { ...current, [field]: value, }; if (field === "name" && !slugTouched) { next.projectSlug = buildProjectSlug(String(value)); } return next; }); }; return (
{ event.preventDefault(); setSubmitting(true); setError(null); void getApiClient() .createProject({ name: form.name, projectSlug: form.projectSlug, repoUrl: form.repoUrl, defaultBranch: form.defaultBranch, configPath: form.configPath || undefined, repoToken: form.repoToken ? form.repoToken : undefined, dispatchMode: form.dispatchMode, }) .then((response) => { pushToast({ tone: "success", title: "Project created", message: `${response.project.ownerSlug}/${response.project.projectSlug} is now tracked.`, }); navigate("/app/projects", { replace: true }); }) .catch((reason: unknown) => { const message = formatApiError(reason); setError(message); pushToast({ tone: "error", title: "Project creation failed", message, }); }) .finally(() => { setSubmitting(false); }); }} >
updateField("name", event.target.value)} placeholder="Docs Forge" helperText="Operator-facing project label shown across the shell." required /> { setSlugTouched(true); updateField("projectSlug", buildProjectSlug(event.target.value)); }} placeholder="docs-forge" helperText="Stable owner-scoped identifier used in URLs." required />
updateField("repoUrl", event.target.value)} placeholder="https://github.com/owner/repo" helperText="HTTPS clone URL only in v1." required />
updateField("defaultBranch", event.target.value)} placeholder="main" helperText="Branch used for manual runs unless overridden." required /> updateField("configPath", event.target.value)} placeholder=".anvil.yml" helperText="Repository-defined pipeline config path." />
updateField("repoToken", event.target.value)} placeholder="Optional personal access token" helperText="Leave empty for public repos." /> {error ? : null}
Cancel
{/* Sidebar */}

Repository host

{form.repoUrl ? inferRepositoryProvider(form.repoUrl) : "not set"}

Dispatch

{(["queue", "workflows"] as const).map((option) => ( ))}

{form.dispatchMode === "queue" ? "Runs are dispatched via Cloudflare Queues with at-least-once delivery." : "Runs use Cloudflare Workflows for durable execution with automatic retries."}

); };