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 { repo } = Route.useParams();
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}