new.tsx

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