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}