import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { ArrowUp, CircleAlert, Github, LoaderCircle } from "lucide-react"; import { ApiError, api, navigateToLogin } from "../shared/api/client"; import { useMe } from "../features/auth/useAuth"; import { Button } from "./Button"; function mapError(err: unknown): string { if (!(err instanceof ApiError)) { return "Something went wrong. Please try again."; } const reason = String(err.context?.reason ?? ""); const message = String(err.context?.message ?? ""); if (err.status === 401) { return "Please sign in with GitHub to submit a repository."; } if (err.status === 403) { return ( message || "Only accounts with admin or maintain permission on this repository can submit it." ); } if (reason === "ref_not_found" || reason === "invalid_ref") { return "That branch/tag/commit was not found in this repository."; } if (reason === "invalid_github_url") { return "That doesn't look like a GitHub repository URL. Expected: https://github.com/owner/repo"; } if (reason === "private_repo") { return "This repository is private or does not exist. OpenVuln scans public projects only."; } if (reason === "cooldown") { const days = err.context?.retry_after_days; return `This project was scanned recently. You can resubmit after ${days ?? "a few"} day(s).`; } if (reason === "duplicate" || err.status === 409) { return message || "This project is already on OpenVuln."; } if (err.status === 404) { return "This repository is private or does not exist. OpenVuln scans public projects only."; } if (err.status >= 500) { return "OpenVuln is temporarily unavailable. Please try again in a moment."; } return message || err.message || "Submission failed."; } export function RepoSubmitForm({ size = "default", appearance = "default", className = "", align = "center", }: { size?: "default" | "hero"; appearance?: "default" | "dark"; className?: string; align?: "center" | "left"; }) { const nav = useNavigate(); const meQ = useMe(); const [url, setUrl] = useState(""); const [ref, setRef] = useState(""); const [showRef, setShowRef] = useState(false); const [error, setError] = useState(null); const [pending, setPending] = useState(false); const hero = size === "hero"; const dark = appearance === "dark"; const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); const git_url = url.trim(); if (!git_url) { setError("Please paste a GitHub repository URL."); return; } // 未登录 → 整页跳 GitHub OAuth,登录后回到部署首页 if (meQ.data && !meQ.data.authenticated) { navigateToLogin(); window.dispatchEvent(new Event("ov-oauth-popup-opened")); return; } setPending(true); try { const res = await api.submitProject({ git_url, ...(ref.trim() ? { ref: ref.trim() } : {}) }); nav(`/p/${res.project.owner_login}/${res.project.name}`, { state: { justSubmitted: true }, }); } catch (err) { setError(mapError(err)); } finally { setPending(false); } }; if (dark) { // hero composer(暗色控制台);appearance prop 名保留为调用方兼容 const centered = align === "center"; return (
void onSubmit(e)} className={className} aria-label="Submit a GitHub repository" aria-busy={pending} >
{ setUrl(e.target.value); setError(null); }} placeholder="Paste a public GitHub repository URL" spellCheck={false} className="min-w-0 flex-1 bg-transparent py-3 font-mono text-[13px] text-ink outline-none placeholder:text-ink-tertiary sm:text-[14px]" aria-label="GitHub repository URL" aria-invalid={!!error} />
{showRef ? ( setRef(e.target.value)} placeholder="Branch, tag, or commit SHA (optional)" spellCheck={false} className="h-8 w-72 rounded-lg border border-line bg-surface-raised px-3 font-mono text-xs text-ink outline-none placeholder:text-ink-tertiary focus:border-line-strong" aria-label="Version to scan (branch, tag, or commit SHA)" /> ) : ( )}
{error ? ( {error} ) : pending ? ( Submitting repository… ) : ( Public repositories · Repository maintainers only )}
); } return (
void onSubmit(e)} className={className}>
setUrl(e.target.value)} placeholder="https://github.com/owner/repo" spellCheck={false} className={`h-12 flex-1 rounded-lg border bg-surface-raised px-3.5 font-mono text-sm text-ink placeholder:text-ink-tertiary focus-ring ${ error ? "border-danger" : "border-line" }`} aria-invalid={!!error} />
{showRef ? ( setRef(e.target.value)} placeholder="Branch, tag, or commit SHA (optional — default: default branch HEAD)" spellCheck={false} className="h-9 w-full rounded-md border border-line bg-surface-raised px-3 font-mono text-xs text-ink placeholder:text-ink-tertiary focus-ring" aria-label="Version to scan (branch, tag, or commit SHA)" /> ) : ( )}
{error && (

{error}

)}
); }