7.0 KB226 lines
Blame
1"use client";
2
3import { useEffect, useState } from "react";
4import { useParams, useRouter } from "next/navigation";
5import Link from "next/link";
6import { useAuth } from "@/lib/auth";
7import { repos, diffs, type Bookmark } from "@/lib/api";
8import { Select } from "@/app/components/ui";
9
10export default function NewDiffPage() {
11 const { owner, repo } = useParams<{ owner: string; repo: string }>();
12 const { user, loading: authLoading } = useAuth();
13 const router = useRouter();
14
15 const [branches, setBranches] = useState<Bookmark[]>([]);
16 const [defaultBranch, setDefaultBranch] = useState("main");
17 const [loadingBranches, setLoadingBranches] = useState(true);
18
19 const [headBranch, setHeadBranch] = useState("");
20 const [title, setTitle] = useState("");
21 const [description, setDescription] = useState("");
22 const [creating, setCreating] = useState(false);
23 const [error, setError] = useState("");
24
25 useEffect(() => {
26 document.title = `New Diff \u00b7 ${repo}`;
27 }, [repo]);
28
29 useEffect(() => {
30 if (!authLoading && !user) {
31 router.push(`/login?redirect=${encodeURIComponent(window.location.pathname)}`);
32 }
33 }, [authLoading, user, router]);
34
35 useEffect(() => {
36 if (!owner || !repo) return;
37 setLoadingBranches(true);
38 Promise.all([
39 repos.get(owner, repo),
40 repos.bookmarks(owner, repo),
41 ])
42 .then(([repoData, bookmarkData]) => {
43 setDefaultBranch(repoData.repo.default_branch || "main");
44 const allBranches = bookmarkData.bookmarks || repoData.branches || [];
45 setBranches(allBranches);
46 // Pre-select a non-default branch if one exists
47 const nonDefault = allBranches.find(
48 (b) => b.name !== (repoData.repo.default_branch || "main")
49 );
50 if (nonDefault) {
51 setHeadBranch(nonDefault.name);
52 }
53 })
54 .catch(() => {})
55 .finally(() => setLoadingBranches(false));
56 }, [owner, repo]);
57
58 async function handleSubmit(e: React.FormEvent) {
59 e.preventDefault();
60 if (!title.trim() || !headBranch) return;
61 setCreating(true);
62 setError("");
63 try {
64 const branch = branches.find((b) => b.name === headBranch);
65 if (!branch) {
66 setError("Branch not found");
67 return;
68 }
69 const defaultBr = branches.find((b) => b.name === defaultBranch);
70 const { diff } = await diffs.create(owner, repo, {
71 title: title.trim(),
72 description: description.trim() || undefined,
73 head_commit: branch.commit_id,
74 base_commit: defaultBr?.commit_id,
75 });
76 router.push(`/${owner}/${repo}/diffs/${diff.number}`);
77 } catch (err: unknown) {
78 setError(err instanceof Error ? err.message : "Failed to create diff");
79 } finally {
80 setCreating(false);
81 }
82 }
83
84 if (authLoading || !user) return null;
85
86 const nonDefaultBranches = branches.filter((b) => b.name !== defaultBranch);
87
88 return (
89 <div className="max-w-xl mx-auto px-4 py-8">
90 <div className="text-sm mb-4">
91 <Link
92 href={`/${owner}/${repo}/diffs`}
93 style={{ color: "var(--text-muted)" }}
94 className="hover:underline"
95 >
96 Diffs
97 </Link>
98 </div>
99 <form
100 onSubmit={handleSubmit}
101 className="p-4 space-y-2.5"
102 style={{
103 backgroundColor: "var(--bg-card)",
104 border: "1px solid var(--border-subtle)",
105 }}
106 >
107 <h1 className="text-base">New diff</h1>
108
109 <label className="block text-xs" style={{ color: "var(--text-muted)" }}>
110 Branch
111 </label>
112 {loadingBranches ? (
113 <div
114 className="w-full px-3 py-2 text-sm"
115 style={{
116 backgroundColor: "var(--bg-input)",
117 border: "1px solid var(--border-subtle)",
118 color: "var(--text-faint)",
119 }}
120 >
121 Loading branches...
122 </div>
123 ) : nonDefaultBranches.length === 0 ? (
124 <div
125 className="text-sm px-3 py-2"
126 style={{
127 backgroundColor: "var(--bg-inset)",
128 border: "1px solid var(--border-subtle)",
129 color: "var(--text-muted)",
130 }}
131 >
132 No branches to diff. Push a branch other than{" "}
133 <code className="text-xs font-mono">{defaultBranch}</code> to create a diff.
134 </div>
135 ) : (
136 <Select
137 value={headBranch}
138 onChange={(e) => setHeadBranch(e.target.value)}
139 options={nonDefaultBranches.map((b) => ({
140 value: b.name,
141 label: `${b.name} \u2192 ${defaultBranch}`,
142 }))}
143 aria-label="Branch"
144 />
145 )}
146
147 <label className="block text-xs" style={{ color: "var(--text-muted)" }}>
148 Title
149 </label>
150 <input
151 value={title}
152 onChange={(e) => setTitle(e.target.value)}
153 className="w-full px-3 py-2 text-sm focus:outline-none"
154 style={{
155 backgroundColor: "var(--bg-input)",
156 border: "1px solid var(--border-subtle)",
157 color: "var(--text-primary)",
158 outline: "none",
159 boxShadow: "none",
160 }}
161 placeholder="What does this change?"
162 required
163 autoFocus
164 />
165
166 <label className="block text-xs" style={{ color: "var(--text-muted)" }}>
167 Description
168 </label>
169 <textarea
170 value={description}
171 onChange={(e) => setDescription(e.target.value)}
172 rows={3}
173 className="w-full px-3 py-2 text-sm focus:outline-none resize-y"
174 style={{
175 backgroundColor: "var(--bg-input)",
176 border: "1px solid var(--border-subtle)",
177 color: "var(--text-primary)",
178 outline: "none",
179 boxShadow: "none",
180 }}
181 placeholder="Optional description"
182 />
183
184 {error && (
185 <div
186 className="text-sm px-3 py-2"
187 style={{
188 backgroundColor: "var(--error-bg)",
189 border: "1px solid var(--error-border)",
190 color: "var(--error-text)",
191 }}
192 >
193 {error}
194 </div>
195 )}
196
197 <div className="flex items-center gap-2">
198 <button
199 type="submit"
200 disabled={creating || !headBranch || !title.trim()}
201 className="px-3 py-1.5 text-sm"
202 style={{
203 backgroundColor: "var(--accent)",
204 color: "var(--accent-text)",
205 opacity: creating || !headBranch || !title.trim() ? 0.5 : 1,
206 }}
207 >
208 {creating ? "Creating..." : "Create diff"}
209 </button>
210 <Link
211 href={`/${owner}/${repo}/diffs`}
212 className="px-3 py-1.5 text-sm"
213 style={{
214 backgroundColor: "var(--bg-inset)",
215 border: "1px solid var(--border-subtle)",
216 color: "var(--text-secondary)",
217 }}
218 >
219 Cancel
220 </Link>
221 </div>
222 </form>
223 </div>
224 );
225}
226