web/app/%5Bowner%5D/%5Brepo%5D/diffs/new/page.tsxblame
View source
55e95011"use client";
55e95012
55e95013import { useEffect, useState } from "react";
55e95014import { useParams, useRouter } from "next/navigation";
55e95015import Link from "next/link";
55e95016import { useAuth } from "@/lib/auth";
55e95017import { repos, diffs, type Bookmark } from "@/lib/api";
55e95018import { Select } from "@/app/components/ui";
55e95019
55e950110export default function NewDiffPage() {
55e950111 const { owner, repo } = useParams<{ owner: string; repo: string }>();
55e950112 const { user, loading: authLoading } = useAuth();
55e950113 const router = useRouter();
55e950114
55e950115 const [branches, setBranches] = useState<Bookmark[]>([]);
55e950116 const [defaultBranch, setDefaultBranch] = useState("main");
55e950117 const [loadingBranches, setLoadingBranches] = useState(true);
55e950118
55e950119 const [headBranch, setHeadBranch] = useState("");
55e950120 const [title, setTitle] = useState("");
55e950121 const [description, setDescription] = useState("");
55e950122 const [creating, setCreating] = useState(false);
55e950123 const [error, setError] = useState("");
55e950124
55e950125 useEffect(() => {
55e950126 document.title = `New Diff \u00b7 ${repo}`;
55e950127 }, [repo]);
55e950128
55e950129 useEffect(() => {
55e950130 if (!authLoading && !user) {
6dd74de31 router.push(`/login?redirect=${encodeURIComponent(window.location.pathname)}`);
55e950132 }
55e950133 }, [authLoading, user, router]);
55e950134
55e950135 useEffect(() => {
55e950136 if (!owner || !repo) return;
55e950137 setLoadingBranches(true);
55e950138 Promise.all([
55e950139 repos.get(owner, repo),
55e950140 repos.bookmarks(owner, repo),
55e950141 ])
55e950142 .then(([repoData, bookmarkData]) => {
55e950143 setDefaultBranch(repoData.repo.default_branch || "main");
55e950144 const allBranches = bookmarkData.bookmarks || repoData.branches || [];
55e950145 setBranches(allBranches);
55e950146 // Pre-select a non-default branch if one exists
55e950147 const nonDefault = allBranches.find(
55e950148 (b) => b.name !== (repoData.repo.default_branch || "main")
55e950149 );
55e950150 if (nonDefault) {
55e950151 setHeadBranch(nonDefault.name);
55e950152 }
55e950153 })
55e950154 .catch(() => {})
55e950155 .finally(() => setLoadingBranches(false));
55e950156 }, [owner, repo]);
55e950157
55e950158 async function handleSubmit(e: React.FormEvent) {
55e950159 e.preventDefault();
55e950160 if (!title.trim() || !headBranch) return;
55e950161 setCreating(true);
55e950162 setError("");
55e950163 try {
55e950164 const branch = branches.find((b) => b.name === headBranch);
55e950165 if (!branch) {
55e950166 setError("Branch not found");
55e950167 return;
55e950168 }
55e950169 const defaultBr = branches.find((b) => b.name === defaultBranch);
55e950170 const { diff } = await diffs.create(owner, repo, {
55e950171 title: title.trim(),
55e950172 description: description.trim() || undefined,
55e950173 head_commit: branch.commit_id,
55e950174 base_commit: defaultBr?.commit_id,
55e950175 });
55e950176 router.push(`/${owner}/${repo}/diffs/${diff.number}`);
55e950177 } catch (err: unknown) {
55e950178 setError(err instanceof Error ? err.message : "Failed to create diff");
55e950179 } finally {
55e950180 setCreating(false);
55e950181 }
55e950182 }
55e950183
55e950184 if (authLoading || !user) return null;
55e950185
55e950186 const nonDefaultBranches = branches.filter((b) => b.name !== defaultBranch);
55e950187
55e950188 return (
55e950189 <div className="max-w-xl mx-auto px-4 py-8">
55e950190 <div className="text-sm mb-4">
55e950191 <Link
55e950192 href={`/${owner}/${repo}/diffs`}
55e950193 style={{ color: "var(--text-muted)" }}
55e950194 className="hover:underline"
55e950195 >
55e950196 Diffs
55e950197 </Link>
55e950198 </div>
55e950199 <form
55e9501100 onSubmit={handleSubmit}
55e9501101 className="p-4 space-y-2.5"
55e9501102 style={{
55e9501103 backgroundColor: "var(--bg-card)",
55e9501104 border: "1px solid var(--border-subtle)",
55e9501105 }}
55e9501106 >
55e9501107 <h1 className="text-base">New diff</h1>
55e9501108
55e9501109 <label className="block text-xs" style={{ color: "var(--text-muted)" }}>
55e9501110 Branch
55e9501111 </label>
55e9501112 {loadingBranches ? (
55e9501113 <div
55e9501114 className="w-full px-3 py-2 text-sm"
55e9501115 style={{
55e9501116 backgroundColor: "var(--bg-input)",
55e9501117 border: "1px solid var(--border-subtle)",
55e9501118 color: "var(--text-faint)",
55e9501119 }}
55e9501120 >
55e9501121 Loading branches...
55e9501122 </div>
55e9501123 ) : nonDefaultBranches.length === 0 ? (
55e9501124 <div
55e9501125 className="text-sm px-3 py-2"
55e9501126 style={{
55e9501127 backgroundColor: "var(--bg-inset)",
55e9501128 border: "1px solid var(--border-subtle)",
55e9501129 color: "var(--text-muted)",
55e9501130 }}
55e9501131 >
55e9501132 No branches to diff. Push a branch other than{" "}
55e9501133 <code className="text-xs font-mono">{defaultBranch}</code> to create a diff.
55e9501134 </div>
55e9501135 ) : (
55e9501136 <Select
55e9501137 value={headBranch}
55e9501138 onChange={(e) => setHeadBranch(e.target.value)}
55e9501139 options={nonDefaultBranches.map((b) => ({
55e9501140 value: b.name,
55e9501141 label: `${b.name} \u2192 ${defaultBranch}`,
55e9501142 }))}
55e9501143 aria-label="Branch"
55e9501144 />
55e9501145 )}
55e9501146
55e9501147 <label className="block text-xs" style={{ color: "var(--text-muted)" }}>
55e9501148 Title
55e9501149 </label>
55e9501150 <input
55e9501151 value={title}
55e9501152 onChange={(e) => setTitle(e.target.value)}
55e9501153 className="w-full px-3 py-2 text-sm focus:outline-none"
55e9501154 style={{
55e9501155 backgroundColor: "var(--bg-input)",
55e9501156 border: "1px solid var(--border-subtle)",
55e9501157 color: "var(--text-primary)",
55e9501158 outline: "none",
55e9501159 boxShadow: "none",
55e9501160 }}
55e9501161 placeholder="What does this change?"
55e9501162 required
55e9501163 autoFocus
55e9501164 />
55e9501165
55e9501166 <label className="block text-xs" style={{ color: "var(--text-muted)" }}>
55e9501167 Description
55e9501168 </label>
55e9501169 <textarea
55e9501170 value={description}
55e9501171 onChange={(e) => setDescription(e.target.value)}
55e9501172 rows={3}
55e9501173 className="w-full px-3 py-2 text-sm focus:outline-none resize-y"
55e9501174 style={{
55e9501175 backgroundColor: "var(--bg-input)",
55e9501176 border: "1px solid var(--border-subtle)",
55e9501177 color: "var(--text-primary)",
55e9501178 outline: "none",
55e9501179 boxShadow: "none",
55e9501180 }}
55e9501181 placeholder="Optional description"
55e9501182 />
55e9501183
55e9501184 {error && (
55e9501185 <div
55e9501186 className="text-sm px-3 py-2"
55e9501187 style={{
55e9501188 backgroundColor: "var(--error-bg)",
55e9501189 border: "1px solid var(--error-border)",
55e9501190 color: "var(--error-text)",
55e9501191 }}
55e9501192 >
55e9501193 {error}
55e9501194 </div>
55e9501195 )}
55e9501196
55e9501197 <div className="flex items-center gap-2">
55e9501198 <button
55e9501199 type="submit"
55e9501200 disabled={creating || !headBranch || !title.trim()}
55e9501201 className="px-3 py-1.5 text-sm"
55e9501202 style={{
55e9501203 backgroundColor: "var(--accent)",
55e9501204 color: "var(--accent-text)",
55e9501205 opacity: creating || !headBranch || !title.trim() ? 0.5 : 1,
55e9501206 }}
55e9501207 >
55e9501208 {creating ? "Creating..." : "Create diff"}
55e9501209 </button>
55e9501210 <Link
55e9501211 href={`/${owner}/${repo}/diffs`}
55e9501212 className="px-3 py-1.5 text-sm"
55e9501213 style={{
55e9501214 backgroundColor: "var(--bg-inset)",
55e9501215 border: "1px solid var(--border-subtle)",
55e9501216 color: "var(--text-secondary)",
55e9501217 }}
55e9501218 >
55e9501219 Cancel
55e9501220 </Link>
55e9501221 </div>
55e9501222 </form>
55e9501223 </div>
55e9501224 );
55e9501225}