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