NewBugPage.tsx

  1import { useNavigate, Link } from "@tanstack/react-router";
  2import { ArrowLeft } from "lucide-react";
  3import { useState } from "react";
  4
  5import { useBugCreateMutation } from "@/__generated__/graphql";
  6import { Markdown } from "@/components/content/Markdown";
  7import { Button } from "@/components/ui/button";
  8import { Input } from "@/components/ui/input";
  9import { Textarea } from "@/components/ui/textarea";
 10import { useRepo } from "@/lib/repo";
 11
 12// New issue form (/:repo/issues/new). Title + body with write/preview tabs.
 13export function NewBugPage() {
 14  const navigate = useNavigate();
 15  const repo = useRepo();
 16  const [title, setTitle] = useState("");
 17  const [message, setMessage] = useState("");
 18  const [preview, setPreview] = useState(false);
 19
 20  const [createBug, { loading, error }] = useBugCreateMutation();
 21
 22  async function handleSubmit(e: React.FormEvent) {
 23    e.preventDefault();
 24    if (!title.trim()) return;
 25    const result = await createBug({
 26      variables: { input: { title: title.trim(), message: message.trim() } },
 27    });
 28    const humanId = result.data?.bugCreate.bug.humanId;
 29    if (humanId) {
 30      void navigate({ to: repo ? `/${repo}/issues/${humanId}` : `/issues/${humanId}` });
 31    }
 32  }
 33
 34  const issuesHref = repo ? `/${repo}/issues` : "/issues";
 35
 36  return (
 37    <div className="mx-auto max-w-3xl">
 38      <Link
 39        to={issuesHref}
 40        className="text-muted-foreground hover:text-foreground mb-6 flex items-center gap-1.5 text-sm"
 41      >
 42        <ArrowLeft className="size-3.5" />
 43        Back to issues
 44      </Link>
 45
 46      <h1 className="mb-6 text-xl font-semibold">New issue</h1>
 47
 48      <form
 49        onSubmit={(e) => {
 50          void handleSubmit(e);
 51        }}
 52        className="space-y-4"
 53      >
 54        <div>
 55          <label htmlFor="title" className="mb-1.5 block text-sm font-medium">
 56            Title
 57          </label>
 58          <Input
 59            id="title"
 60            placeholder="Brief description of the issue"
 61            value={title}
 62            onChange={(e) => setTitle(e.target.value)}
 63            required
 64            disabled={loading}
 65          />
 66        </div>
 67
 68        <div>
 69          <div className="mb-1.5 flex items-center justify-between">
 70            <label className="text-sm font-medium">Description</label>
 71            <div className="flex gap-1 text-sm">
 72              <button
 73                type="button"
 74                onClick={() => setPreview(false)}
 75                className={`rounded-sm px-2 py-0.5 transition-colors ${
 76                  !preview ? "bg-muted font-medium" : "text-muted-foreground hover:text-foreground"
 77                }`}
 78              >
 79                Write
 80              </button>
 81              <button
 82                type="button"
 83                onClick={() => setPreview(true)}
 84                disabled={!message.trim()}
 85                className={`rounded-sm px-2 py-0.5 transition-colors disabled:opacity-40 ${
 86                  preview ? "bg-muted font-medium" : "text-muted-foreground hover:text-foreground"
 87                }`}
 88              >
 89                Preview
 90              </button>
 91            </div>
 92          </div>
 93
 94          {preview ? (
 95            <div className="border-input min-h-[200px] rounded-md border px-3 py-2">
 96              <Markdown content={message} />
 97            </div>
 98          ) : (
 99            <Textarea
100              placeholder="Describe the issue in detail…"
101              className="min-h-[200px]"
102              value={message}
103              onChange={(e) => setMessage(e.target.value)}
104              disabled={loading}
105            />
106          )}
107        </div>
108
109        {error && (
110          <p className="text-destructive text-sm">Failed to create issue: {error.message}</p>
111        )}
112
113        <div className="flex justify-end gap-2">
114          <Button
115            type="button"
116            variant="ghost"
117            onClick={() => {
118              void navigate({ to: issuesHref });
119            }}
120            disabled={loading}
121          >
122            Cancel
123          </Button>
124          <Button type="submit" disabled={!title.trim() || loading}>
125            {loading ? "Creating…" : "Submit new issue"}
126          </Button>
127        </div>
128      </form>
129    </div>
130  );
131}