{"path":"apps/dashboard/src/pages/create-job.tsx","size":16609,"locked":false,"binary":false,"truncated":false,"content":"import { useState, useEffect } from \"react\";\nimport { useLocation } from \"wouter\";\nimport { Plus, Trash2, Wallet, ExternalLink, Loader2, Sparkles } from \"lucide-react\";\nimport { useWallet } from \"@solana/wallet-adapter-react\";\nimport { BN } from \"@coral-xyz/anchor\";\nimport { useCreateJob } from \"@workspace/api-client-react\";\nimport { useMappersClient } from \"@/hooks/use-mappers-client\";\nimport { WalletButton } from \"@/components/wallet-button\";\n\nconst INPUT =\n  \"w-full px-4 py-3 text-sm font-mono glass rounded-xl text-white/80 placeholder:text-white/20 outline-none focus:border-emerald-500/40 focus:shadow-[0_0_16px_rgba(20,241,149,0.06)] transition-all duration-200 min-h-[44px] bg-transparent\";\n\nfunction Field({\n  label,\n  hint,\n  children,\n}: {\n  label: string;\n  hint?: string;\n  children: React.ReactNode;\n}) {\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <label className=\"text-[9px] font-mono font-bold text-white/30 uppercase tracking-[0.15em]\">\n        {label}\n      </label>\n      {children}\n      {hint && (\n        <p className=\"text-[10px] font-mono text-white/20 leading-relaxed\">{hint}</p>\n      )}\n    </div>\n  );\n}\n\ntype TxPhase = \"idle\" | \"signing\" | \"confirming\" | \"done\";\n\nexport default function CreateJob() {\n  const [, navigate] = useLocation();\n  const createJob = useCreateJob();\n  const { publicKey, connected } = useWallet();\n  const mappersClient = useMappersClient();\n\n  const [form, setForm] = useState({\n    jobId: \"\",\n    clientPubkey: \"\",\n    freelancerPubkey: \"\",\n    oraclePubkey: \"\",\n    amountSol: \"\",\n    description: \"\",\n  });\n  const [criteria, setCriteria] = useState<string[]>([\"\"]);\n  const [error, setError] = useState<string | null>(null);\n  const [txPhase, setTxPhase] = useState<TxPhase>(\"idle\");\n  const [txSig, setTxSig] = useState<string | null>(null);\n  const [onChain, setOnChain] = useState(false);\n\n  useEffect(() => {\n    if (publicKey && !form.clientPubkey) {\n      setForm((f) => ({ ...f, clientPubkey: publicKey.toBase58() }));\n    }\n  }, [publicKey]);\n\n  const set =\n    (key: keyof typeof form) =>\n    (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>\n      setForm((f) => ({ ...f, [key]: e.target.value }));\n\n  const addCriterion = () => setCriteria((c) => [...c, \"\"]);\n  const removeCriterion = (i: number) =>\n    setCriteria((c) => c.filter((_, idx) => idx !== i));\n  const setCriterion = (i: number, val: string) =>\n    setCriteria((c) => c.map((v, idx) => (idx === i ? val : v)));\n\n  function validate(): { amountLamports: number; acceptanceCriteria: string[] } | null {\n    const amountLamports = Math.round(parseFloat(form.amountSol) * 1e9);\n    if (isNaN(amountLamports) || amountLamports <= 0) {\n      setError(\"Enter a valid positive SOL amount.\");\n      return null;\n    }\n    if (!form.jobId.trim()) { setError(\"Job ID is required.\"); return null; }\n    if (form.jobId.trim().length > 32) { setError(\"Job ID must be ≤ 32 characters.\"); return null; }\n    if (!form.clientPubkey.trim()) { setError(\"Client public key is required.\"); return null; }\n    if (!form.freelancerPubkey.trim()) { setError(\"Freelancer public key is required.\"); return null; }\n    if (!form.oraclePubkey.trim()) { setError(\"Oracle public key is required.\"); return null; }\n    return { amountLamports, acceptanceCriteria: criteria.filter((c) => c.trim() !== \"\") };\n  }\n\n  function registerInDb(sig?: string) {\n    const result = validate();\n    if (!result) return;\n    const { amountLamports, acceptanceCriteria } = result;\n\n    const desc = [\n      form.description.trim() || undefined,\n      sig ? `on-chain tx: ${sig}` : undefined,\n    ]\n      .filter(Boolean)\n      .join(\" | \");\n\n    createJob.mutate(\n      {\n        data: {\n          jobId: form.jobId.trim(),\n          clientPubkey: form.clientPubkey.trim(),\n          freelancerPubkey: form.freelancerPubkey.trim(),\n          oraclePubkey: form.oraclePubkey.trim(),\n          amountLamports: amountLamports.toString(),\n          description: desc || undefined,\n          acceptanceCriteria: acceptanceCriteria.length > 0 ? acceptanceCriteria : undefined,\n        },\n      },\n      {\n        onSuccess: (job) => navigate(`/jobs/${job.jobId}`),\n        onError: (err) =>\n          setError(err instanceof Error ? err.message : \"Failed to register job.\"),\n      }\n    );\n  }\n\n  const handleRegisterOnly = (e: React.FormEvent) => {\n    e.preventDefault();\n    setError(null);\n    setOnChain(false);\n    registerInDb();\n  };\n\n  const handleInitOnChain = async (e: React.FormEvent) => {\n    e.preventDefault();\n    setError(null);\n    setOnChain(true);\n\n    if (!mappersClient || !publicKey) {\n      setError(\"Connect your wallet first.\");\n      return;\n    }\n\n    const result = validate();\n    if (!result) return;\n    const { amountLamports } = result;\n\n    let sig: string;\n    try {\n      const { PublicKey } = await import(\"@solana/web3.js\");\n      setTxPhase(\"signing\");\n\n      sig = await mappersClient.initializeJob({\n        jobId: form.jobId.trim(),\n        amount: new BN(amountLamports),\n        freelancer: new PublicKey(form.freelancerPubkey.trim()),\n        oracle: new PublicKey(form.oraclePubkey.trim()),\n      });\n\n      setTxPhase(\"confirming\");\n      setTxSig(sig);\n    } catch (err) {\n      setTxPhase(\"idle\");\n      setError(\n        err instanceof Error\n          ? err.message.replace(/\\n/g, \" \").slice(0, 200)\n          : \"Transaction failed.\"\n      );\n      return;\n    }\n\n    setTxPhase(\"done\");\n    registerInDb(sig);\n  };\n\n  const busy = createJob.isPending || txPhase === \"signing\" || txPhase === \"confirming\";\n\n  return (\n    <div className=\"flex-1 flex flex-col\">\n      <div className=\"relative px-5 md:px-8 py-6 md:py-7 border-b border-white/[0.05] overflow-hidden\">\n        <div className=\"absolute inset-0 bg-gradient-to-r from-emerald-500/[0.03] via-transparent to-purple-500/[0.02]\" />\n        <div className=\"relative\">\n          <h1 className=\"text-2xl md:text-3xl font-bold tracking-tight text-white\">Create Escrow</h1>\n          <p className=\"text-[10px] text-white/30 font-mono mt-1\">\n            Initialize an on-chain escrow and register in the protocol database\n          </p>\n        </div>\n      </div>\n\n      <div className=\"flex-1 overflow-y-auto p-5 md:p-8\">\n        <div className=\"max-w-xl mx-auto space-y-5\">\n\n          {/* Wallet status banner */}\n          <div className={`glass rounded-2xl px-5 py-4 text-[11px] font-mono leading-relaxed border-l-2 ${\n            connected\n              ? \"border-l-emerald-500/50 bg-emerald-500/[0.04]\"\n              : \"border-l-amber-500/50 bg-amber-500/[0.04]\"\n          }`}>\n            {connected ? (\n              <span className=\"text-white/50\">\n                Wallet connected —{\" \"}\n                <span className=\"text-emerald-400\">{publicKey?.toBase58().slice(0, 8)}…{publicKey?.toBase58().slice(-6)}</span>\n                . Use <strong className=\"text-white/80\">Register + Init On-Chain</strong> to sign the{\" \"}\n                <code className=\"text-emerald-400/80 bg-emerald-400/10 px-1.5 py-0.5 rounded-md\">initialize_gig</code> instruction on devnet.\n              </span>\n            ) : (\n              <span className=\"text-amber-400/70\">\n                Connect your Phantom wallet to sign the on-chain escrow transaction. Or use{\" \"}\n                <strong className=\"text-white/70\">Register Only</strong> to save to the database without an on-chain tx.\n              </span>\n            )}\n          </div>\n\n          {!connected && (\n            <WalletButton />\n          )}\n\n          {/* Tx status */}\n          {txPhase !== \"idle\" && (\n            <div className={`glass rounded-2xl px-5 py-4 text-[11px] font-mono space-y-2 ${\n              txPhase === \"done\"\n                ? \"border-emerald-500/20 bg-emerald-500/[0.04]\"\n                : \"border-white/[0.08]\"\n            }`}>\n              {txPhase === \"signing\" && (\n                <div className=\"flex items-center gap-2.5 text-amber-400\">\n                  <Loader2 className=\"w-3.5 h-3.5 animate-spin\" />\n                  Waiting for wallet signature…\n                </div>\n              )}\n              {txPhase === \"confirming\" && (\n                <div className=\"flex items-center gap-2.5 text-emerald-400\">\n                  <Loader2 className=\"w-3.5 h-3.5 animate-spin\" />\n                  Confirming on devnet…\n                </div>\n              )}\n              {txPhase === \"done\" && txSig && (\n                <>\n                  <div className=\"text-emerald-400 font-bold flex items-center gap-2\">\n                    <Sparkles className=\"w-3.5 h-3.5\" /> On-chain escrow initialized\n                  </div>\n                  <a\n                    href={`https://explorer.solana.com/tx/${txSig}?cluster=devnet`}\n                    target=\"_blank\"\n                    rel=\"noopener noreferrer\"\n                    className=\"flex items-center gap-1.5 text-emerald-400/60 hover:text-emerald-400 transition-colors\"\n                  >\n                    <ExternalLink className=\"w-3 h-3\" />\n                    View on Solana Explorer\n                  </a>\n                </>\n              )}\n            </div>\n          )}\n\n          <form className=\"space-y-5\">\n            <Field label=\"Job ID\" hint=\"Max 32 characters. Use a short unique identifier (e.g. gig-001).\">\n              <input\n                className={INPUT}\n                placeholder=\"gig-001\"\n                value={form.jobId}\n                onChange={set(\"jobId\")}\n                maxLength={32}\n                required\n                autoComplete=\"off\"\n                autoCapitalize=\"none\"\n              />\n            </Field>\n\n            <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-5\">\n              <Field label=\"Client Public Key\" hint={connected ? \"Auto-filled from wallet.\" : \"Base58 Solana address.\"}>\n                <div className=\"relative\">\n                  <input\n                    className={`${INPUT} ${connected ? \"pr-9\" : \"\"}`}\n                    placeholder=\"6LUVzT…rFq9W\"\n                    value={form.clientPubkey}\n                    onChange={set(\"clientPubkey\")}\n                    required\n                    autoComplete=\"off\"\n                    autoCapitalize=\"none\"\n                    spellCheck={false}\n                  />\n                  {connected && (\n                    <Wallet className=\"absolute right-3.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-emerald-400/50 pointer-events-none\" />\n                  )}\n                </div>\n              </Field>\n              <Field label=\"Freelancer Public Key\" hint=\"Base58 Solana address.\">\n                <input\n                  className={INPUT}\n                  placeholder=\"9Kx2bP…uMn3R\"\n                  value={form.freelancerPubkey}\n                  onChange={set(\"freelancerPubkey\")}\n                  required\n                  autoComplete=\"off\"\n                  autoCapitalize=\"none\"\n                  spellCheck={false}\n                />\n              </Field>\n            </div>\n\n            <Field label=\"Oracle Public Key\" hint=\"The AI oracle keypair that will adjudicate this escrow.\">\n              <input\n                className={INPUT}\n                placeholder=\"52yt1g…KX2Mu\"\n                value={form.oraclePubkey}\n                onChange={set(\"oraclePubkey\")}\n                required\n                autoComplete=\"off\"\n                autoCapitalize=\"none\"\n                spellCheck={false}\n              />\n            </Field>\n\n            <Field label=\"Amount (SOL)\" hint=\"Escrowed SOL. Minimum ~0.000890880 SOL (rent-exempt).\">\n              <input\n                className={INPUT}\n                type=\"number\"\n                step=\"0.000000001\"\n                min=\"0.000000001\"\n                inputMode=\"decimal\"\n                placeholder=\"1.5\"\n                value={form.amountSol}\n                onChange={set(\"amountSol\")}\n                required\n              />\n            </Field>\n\n            <Field label=\"Description\" hint=\"Optional: describe the work being commissioned.\">\n              <textarea\n                className={`${INPUT} resize-none`}\n                rows={3}\n                placeholder=\"Build a Solana token staking UI…\"\n                value={form.description}\n                onChange={set(\"description\")}\n              />\n            </Field>\n\n            <Field\n              label=\"Acceptance Criteria\"\n              hint=\"Each criterion the AI oracle will evaluate against.\"\n            >\n              <div className=\"space-y-2.5\">\n                {criteria.map((c, i) => (\n                  <div key={i} className=\"flex gap-2\">\n                    <input\n                      className={`${INPUT} flex-1`}\n                      placeholder={`Criterion ${i + 1}…`}\n                      value={c}\n                      onChange={(e) => setCriterion(i, e.target.value)}\n                    />\n                    {criteria.length > 1 && (\n                      <button\n                        type=\"button\"\n                        onClick={() => removeCriterion(i)}\n                        className=\"w-11 h-11 flex items-center justify-center glass rounded-xl text-rose-400/50 hover:text-rose-400 hover:border-rose-400/30 active:scale-95 transition-all duration-200 shrink-0\"\n                        aria-label=\"Remove criterion\"\n                      >\n                        <Trash2 className=\"w-3.5 h-3.5\" />\n                      </button>\n                    )}\n                  </div>\n                ))}\n                <button\n                  type=\"button\"\n                  onClick={addCriterion}\n                  className=\"flex items-center gap-1.5 text-xs font-mono text-white/25 hover:text-emerald-400 transition-colors py-1\"\n                >\n                  <Plus className=\"w-3.5 h-3.5\" />\n                  Add criterion\n                </button>\n              </div>\n            </Field>\n\n            {error && (\n              <div className=\"glass rounded-xl px-4 py-3 text-xs font-mono text-rose-400 border-rose-500/20 bg-rose-500/[0.05]\">\n                {error}\n              </div>\n            )}\n\n            <div className=\"flex flex-col sm:flex-row gap-3 pt-2\">\n              {connected && mappersClient && (\n                <button\n                  type=\"submit\"\n                  disabled={busy}\n                  onClick={handleInitOnChain}\n                  className=\"flex-1 flex items-center justify-center gap-2 px-5 py-3.5 text-xs font-mono font-bold rounded-xl bg-gradient-to-r from-emerald-500 to-emerald-400 text-black hover:from-emerald-400 hover:to-emerald-300 active:scale-[0.97] disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-[0_0_24px_rgba(20,241,149,0.2)] hover:shadow-[0_0_32px_rgba(20,241,149,0.35)] min-h-[48px]\"\n                >\n                  {txPhase === \"signing\" ? (\n                    <><Loader2 className=\"w-3.5 h-3.5 animate-spin\" /> Awaiting signature…</>\n                  ) : txPhase === \"confirming\" ? (\n                    <><Loader2 className=\"w-3.5 h-3.5 animate-spin\" /> Confirming…</>\n                  ) : createJob.isPending && onChain ? (\n                    <><Loader2 className=\"w-3.5 h-3.5 animate-spin\" /> Saving…</>\n                  ) : (\n                    \"Register + Init On-Chain\"\n                  )}\n                </button>\n              )}\n\n              <button\n                type=\"submit\"\n                disabled={busy}\n                onClick={handleRegisterOnly}\n                className={`flex-1 px-5 py-3.5 text-xs font-mono font-bold rounded-xl transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed active:scale-[0.97] min-h-[48px] ${\n                  connected && mappersClient\n                    ? \"glass text-white/50 hover:text-white/80 hover:border-white/15\"\n                    : \"bg-gradient-to-r from-emerald-500 to-emerald-400 text-black hover:from-emerald-400 hover:to-emerald-300 shadow-[0_0_24px_rgba(20,241,149,0.2)]\"\n                }`}\n              >\n                {createJob.isPending && !onChain ? \"Registering…\" : \"Register Only\"}\n              </button>\n\n              <a\n                href=\"/jobs\"\n                className=\"flex-1 px-5 py-3.5 text-xs font-mono text-center glass rounded-xl text-white/30 hover:text-white/60 hover:border-white/15 active:scale-[0.97] transition-all duration-200 min-h-[48px] flex items-center justify-center\"\n              >\n                Cancel\n              </a>\n            </div>\n          </form>\n        </div>\n      </div>\n    </div>\n  );\n}\n"}