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}