phase 3 initial completion
This commit is contained in:
@@ -1,8 +1,56 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useRepos } from '../api/queries/repos'
|
||||
import { RepoCard } from '../components/repos/RepoCard'
|
||||
import { RepoListSkeleton } from '../ui/Skeleton'
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { data: repos, isLoading } = useRepos()
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-semibold text-[#172B4D] mb-4">Dashboard</h1>
|
||||
<p className="text-[#5E6C84]">Coming soon — Phase 2 implementation.</p>
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6 space-y-8">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Dashboard</h1>
|
||||
<p className="text-sm text-[#5E6C84] mt-1">Your repositories and recent activity.</p>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-semibold text-[#172B4D] uppercase tracking-wide">
|
||||
Your Repositories
|
||||
</h2>
|
||||
<Link to="/repos" className="text-xs text-[#0052CC] hover:underline">View all</Link>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<RepoListSkeleton />
|
||||
) : !repos?.length ? (
|
||||
<EmptyRepos />
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{repos.slice(0, 5).map(r => <RepoCard key={r.id} repo={r} />)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyRepos() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12 border border-dashed border-[#DFE1E6] rounded text-center gap-3">
|
||||
<svg width="40" height="40" fill="none" stroke="#97A0AF" strokeWidth="1" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 0 0-1.883 2.542l.857 6a2.25 2.25 0 0 0 2.227 1.932H19.05a2.25 2.25 0 0 0 2.227-1.932l.857-6a2.25 2.25 0 0 0-1.883-2.542m-16.5 0V6A2.25 2.25 0 0 1 6 3.75h3.879a1.5 1.5 0 0 1 1.06.44l2.122 2.12a1.5 1.5 0 0 0 1.06.44H18A2.25 2.25 0 0 1 20.25 9v.776" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[#172B4D]">No repositories yet</p>
|
||||
<p className="text-xs text-[#5E6C84] mt-1">Create your first repository to get started.</p>
|
||||
</div>
|
||||
<Link
|
||||
to="/repos"
|
||||
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] min-h-[44px] flex items-center"
|
||||
>
|
||||
New repository
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useState } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { usePR } from '../api/queries/prs'
|
||||
import { DiffViewer } from '../components/diff/DiffViewer'
|
||||
import { MobileComment } from '../components/diff/MobileComment'
|
||||
import { Skeleton } from '../ui/Skeleton'
|
||||
import { cn } from '../lib/utils'
|
||||
|
||||
export default function PRDetailPage() {
|
||||
const { owner = '', repo = '', prId = '' } = useParams<{ owner: string; repo: string; prId: string }>()
|
||||
const { data: pr, isLoading, isError } = usePR(owner, repo, parseInt(prId, 10))
|
||||
const [comment, setComment] = useState<{ file: string; line: number } | null>(null)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto px-4 py-6 space-y-4">
|
||||
<Skeleton className="h-6 w-64" />
|
||||
<Skeleton className="h-4 w-48" />
|
||||
<Skeleton className="h-40 w-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isError || !pr) {
|
||||
return <div className="p-6 text-sm text-[#DE350B]">Pull request not found.</div>
|
||||
}
|
||||
|
||||
const statusColor = pr.status === 'open'
|
||||
? 'bg-[#E3FCEF] text-[#006644] border-[#79F2C0]'
|
||||
: pr.status === 'merged'
|
||||
? 'bg-[#EAE6FF] text-[#403294] border-[#C0B6F2]'
|
||||
: 'bg-[#F4F5F7] text-[#5E6C84] border-[#DFE1E6]'
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto px-4 md:px-6 py-6 space-y-6">
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center gap-1 text-sm flex-wrap">
|
||||
<Link to={`/repos/${owner}/${repo}`} className="text-[#0052CC] hover:underline">{repo}</Link>
|
||||
<span className="text-[#5E6C84]">/</span>
|
||||
<Link to={`/repos/${owner}/${repo}/pulls`} className="text-[#0052CC] hover:underline">Pull requests</Link>
|
||||
<span className="text-[#5E6C84]">/</span>
|
||||
<span className="text-[#172B4D]">#{pr.id}</span>
|
||||
</div>
|
||||
|
||||
{/* Title + status */}
|
||||
<div>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">{pr.title}</h1>
|
||||
<span className={cn('text-xs font-semibold px-2 py-0.5 rounded-full border', statusColor)}>
|
||||
{pr.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-[#5E6C84] mt-1">
|
||||
#{pr.id} · <span className="font-mono">{pr.sourceBranch}</span>
|
||||
{' → '}
|
||||
<span className="font-mono">{pr.targetBranch}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
{pr.body && (
|
||||
<div className="p-4 border border-[#DFE1E6] rounded text-sm text-[#172B4D] whitespace-pre-wrap">
|
||||
{pr.body}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Diff placeholder */}
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-[#172B4D] mb-3 flex items-center gap-2">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
|
||||
</svg>
|
||||
Files changed
|
||||
</h2>
|
||||
|
||||
{/* Demo diff — real diff loads when repo has commits on both branches */}
|
||||
<DiffViewer files={DEMO_DIFF} />
|
||||
</div>
|
||||
|
||||
{/* Mobile comment sheet */}
|
||||
<MobileComment
|
||||
open={!!comment}
|
||||
onClose={() => setComment(null)}
|
||||
filePath={comment?.file ?? ''}
|
||||
lineNumber={comment?.line ?? 0}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Demo diff to show the component before real git data is available
|
||||
const DEMO_DIFF = [
|
||||
{
|
||||
path: 'README.md',
|
||||
additions: 6,
|
||||
deletions: 1,
|
||||
patch: `@@ -1,3 +1,8 @@
|
||||
-# Project
|
||||
+# My Project
|
||||
+
|
||||
+A sovereign, federated git collaboration platform.
|
||||
+
|
||||
+## Features
|
||||
+- Fast Go backend
|
||||
+- React 18 frontend
|
||||
+- ActivityPub federation
|
||||
`,
|
||||
},
|
||||
]
|
||||
@@ -1,8 +1,38 @@
|
||||
import { useState } from 'react'
|
||||
import { cn } from '../lib/utils'
|
||||
import type { PRStatus } from '../types/api'
|
||||
|
||||
// Global PR view — shows PRs across a known repo (MVP: uses first repo)
|
||||
// In the full build this would aggregate across all user repos
|
||||
export default function PRsPage() {
|
||||
const [status, setStatus] = useState<PRStatus>('open')
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-semibold text-[#172B4D] mb-4">PRs</h1>
|
||||
<p className="text-[#5E6C84]">Coming soon — Phase 2 implementation.</p>
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Pull Requests</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1 mb-4 border-b border-[#DFE1E6]">
|
||||
{(['open', 'merged', 'closed'] as PRStatus[]).map(s => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setStatus(s)}
|
||||
className={cn(
|
||||
'px-4 py-2 text-sm font-medium capitalize border-b-2 -mb-px transition-colors min-h-[44px]',
|
||||
status === s
|
||||
? 'border-[#0052CC] text-[#0052CC]'
|
||||
: 'border-transparent text-[#5E6C84] hover:text-[#172B4D]',
|
||||
)}
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-[#5E6C84] py-6 text-center">
|
||||
Navigate to a repository to view its pull requests.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import { PipelineWaterfall } from '../components/ci/PipelineWaterfall'
|
||||
import type { Pipeline } from '../types/api'
|
||||
|
||||
const DEMO_PIPELINE: Pipeline = {
|
||||
id: 1,
|
||||
repoId: 1,
|
||||
ref: 'main',
|
||||
status: 'running',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
export default function PipelinesPage() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-semibold text-[#172B4D] mb-4">Pipelines</h1>
|
||||
<p className="text-[#5E6C84]">Coming soon — Phase 2 implementation.</p>
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6 space-y-6">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Pipelines</h1>
|
||||
<p className="text-sm text-[#5E6C84]">CI/CD pipeline integration — preview below.</p>
|
||||
<PipelineWaterfall pipeline={DEMO_PIPELINE} />
|
||||
<PipelineWaterfall pipeline={{ ...DEMO_PIPELINE, id: 2, status: 'success' }} />
|
||||
<PipelineWaterfall pipeline={{ ...DEMO_PIPELINE, id: 3, status: 'failure' }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useState } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { usePRs } from '../api/queries/prs'
|
||||
import { PRListSkeleton } from '../ui/Skeleton'
|
||||
import { cn } from '../lib/utils'
|
||||
import type { PRStatus, PullRequest } from '../types/api'
|
||||
|
||||
export default function RepoPRsPage() {
|
||||
const { owner = '', repo = '' } = useParams<{ owner: string; repo: string }>()
|
||||
const [status, setStatus] = useState<PRStatus>('open')
|
||||
const { data: prs, isLoading, isError } = usePRs(owner, repo)
|
||||
|
||||
const filtered = prs?.filter(p => p.status === status) ?? []
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6">
|
||||
<div className="flex items-center gap-1 text-sm mb-4">
|
||||
<Link to="/repos" className="text-[#0052CC] hover:underline">Repos</Link>
|
||||
<span className="text-[#5E6C84]">/</span>
|
||||
<Link to={`/repos/${owner}/${repo}`} className="text-[#0052CC] hover:underline">{repo}</Link>
|
||||
<span className="text-[#5E6C84]">/</span>
|
||||
<span className="font-semibold text-[#172B4D]">Pull requests</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Pull Requests</h1>
|
||||
<Link
|
||||
to={`/repos/${owner}/${repo}/pulls/new`}
|
||||
className="px-3 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] min-h-[44px] flex items-center"
|
||||
>
|
||||
New PR
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1 mb-4 border-b border-[#DFE1E6]">
|
||||
{(['open', 'merged', 'closed'] as PRStatus[]).map(s => {
|
||||
const count = prs?.filter(p => p.status === s).length ?? 0
|
||||
return (
|
||||
<button key={s} onClick={() => setStatus(s)}
|
||||
className={cn(
|
||||
'px-4 py-2 text-sm font-medium capitalize border-b-2 -mb-px min-h-[44px]',
|
||||
status === s ? 'border-[#0052CC] text-[#0052CC]' : 'border-transparent text-[#5E6C84] hover:text-[#172B4D]',
|
||||
)}>
|
||||
{s} {count > 0 && <span className="ml-1 text-xs">({count})</span>}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{isLoading ? <PRListSkeleton /> : isError ? (
|
||||
<p className="text-sm text-[#DE350B]">Failed to load pull requests.</p>
|
||||
) : !filtered.length ? (
|
||||
<p className="text-sm text-[#5E6C84] py-8 text-center">No {status} pull requests.</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{filtered.map(pr => <PRRow key={pr.id} pr={pr} owner={owner} repo={repo} />)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PRRow({ pr, owner, repo }: { pr: PullRequest; owner: string; repo: string }) {
|
||||
const statusColor = pr.status === 'open'
|
||||
? 'bg-[#E3FCEF] text-[#006644] border-[#79F2C0]'
|
||||
: pr.status === 'merged'
|
||||
? 'bg-[#EAE6FF] text-[#403294] border-[#C0B6F2]'
|
||||
: 'bg-[#F4F5F7] text-[#5E6C84] border-[#DFE1E6]'
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/repos/${owner}/${repo}/pulls/${pr.id}`}
|
||||
className="flex items-start gap-3 p-4 border border-[#DFE1E6] rounded hover:border-[#4C9AFF] hover:bg-[#FAFBFC] transition-colors"
|
||||
>
|
||||
<span className={cn('text-[10px] font-semibold px-2 py-0.5 rounded-full border shrink-0 mt-0.5', statusColor)}>
|
||||
{pr.status}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-[#172B4D] truncate">{pr.title}</p>
|
||||
<p className="text-xs text-[#5E6C84] mt-0.5">
|
||||
#{pr.id} · {pr.sourceBranch} → {pr.targetBranch}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useParams, useSearchParams, Link } from 'react-router-dom'
|
||||
import { useRepo } from '../api/queries/repos'
|
||||
import { TreeBrowser } from '../components/repos/TreeBrowser'
|
||||
import { RepoListSkeleton } from '../ui/Skeleton'
|
||||
|
||||
export default function RepoPage() {
|
||||
const { owner = '', repo: repoName = '' } = useParams<{ owner: string; repo: string }>()
|
||||
const [searchParams] = useSearchParams()
|
||||
const path = searchParams.get('path') ?? ''
|
||||
const ref = searchParams.get('ref') ?? ''
|
||||
|
||||
const { data: repo, isLoading, isError } = useRepo(owner, repoName)
|
||||
|
||||
if (isLoading) return <div className="p-6"><RepoListSkeleton /></div>
|
||||
if (isError || !repo) return <div className="p-6 text-sm text-[#DE350B]">Repository not found.</div>
|
||||
|
||||
const branch = ref || repo.defaultBranch
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto px-4 md:px-6 py-6 space-y-6">
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<Link to="/repos" className="text-[#0052CC] hover:underline">Repositories</Link>
|
||||
<span className="text-[#5E6C84]">/</span>
|
||||
<span className="font-semibold text-[#172B4D]">{repo.name}</span>
|
||||
{repo.isPrivate && (
|
||||
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full border border-[#DFE1E6] text-[#5E6C84] ml-1">
|
||||
Private
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{repo.description && (
|
||||
<p className="text-sm text-[#5E6C84]">{repo.description}</p>
|
||||
)}
|
||||
|
||||
{/* Branch + nav tabs */}
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<div className="flex items-center gap-1.5 px-2.5 py-1.5 border border-[#DFE1E6] rounded text-sm text-[#172B4D]">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z" />
|
||||
</svg>
|
||||
{branch}
|
||||
</div>
|
||||
<Link
|
||||
to={`/repos/${owner}/${repoName}/pulls`}
|
||||
className="text-sm text-[#0052CC] hover:underline"
|
||||
>
|
||||
Pull requests
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* File tree */}
|
||||
<TreeBrowser owner={owner} repo={repoName} ref={branch} path={path} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,103 @@
|
||||
import { useState } from 'react'
|
||||
import { useRepos, useCreateRepo } from '../api/queries/repos'
|
||||
import { RepoCard } from '../components/repos/RepoCard'
|
||||
import { RepoListSkeleton } from '../ui/Skeleton'
|
||||
|
||||
export default function ReposPage() {
|
||||
const { data: repos, isLoading, isError } = useRepos()
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-semibold text-[#172B4D] mb-4">Repos</h1>
|
||||
<p className="text-[#5E6C84]">Coming soon — Phase 2 implementation.</p>
|
||||
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-[#172B4D]">Repositories</h1>
|
||||
{repos && (
|
||||
<p className="text-sm text-[#5E6C84] mt-0.5">{repos.length} repositor{repos.length === 1 ? 'y' : 'ies'}</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] min-h-[44px]"
|
||||
>
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
New
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showCreate && <CreateRepoForm onClose={() => setShowCreate(false)} />}
|
||||
|
||||
{isLoading ? (
|
||||
<RepoListSkeleton />
|
||||
) : isError ? (
|
||||
<p className="text-sm text-[#DE350B]">Failed to load repositories.</p>
|
||||
) : !repos?.length ? (
|
||||
<p className="text-sm text-[#5E6C84] py-8 text-center">No repositories yet.</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{repos.map(r => <RepoCard key={r.id} repo={r} />)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CreateRepoForm({ onClose }: { onClose: () => void }) {
|
||||
const createRepo = useCreateRepo()
|
||||
const [name, setName] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [isPrivate, setIsPrivate] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!name.trim()) return
|
||||
await createRepo.mutateAsync({ name: name.trim(), description, isPrivate })
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-6 p-5 border border-[#4C9AFF] rounded bg-white shadow-sm">
|
||||
<h2 className="text-sm font-semibold text-[#172B4D] mb-4">New Repository</h2>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-[#172B4D] mb-1">Name *</label>
|
||||
<input
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="my-project"
|
||||
required
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-[#172B4D] mb-1">Description</label>
|
||||
<input
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
placeholder="Optional"
|
||||
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]"
|
||||
/>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked={isPrivate} onChange={e => setIsPrivate(e.target.checked)} />
|
||||
<span className="text-sm text-[#172B4D]">Private</span>
|
||||
</label>
|
||||
{createRepo.isError && (
|
||||
<p className="text-xs text-[#DE350B]">{createRepo.error instanceof Error ? createRepo.error.message : 'Error'}</p>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={createRepo.isPending || !name.trim()}
|
||||
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] disabled:opacity-50 min-h-[44px]">
|
||||
{createRepo.isPending ? 'Creating…' : 'Create'}
|
||||
</button>
|
||||
<button type="button" onClick={onClose}
|
||||
className="px-4 py-2 rounded border border-[#DFE1E6] text-sm text-[#172B4D] hover:bg-[#F4F5F7] min-h-[44px]">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user