can now import repos and have more settings for creating new ones.

This commit is contained in:
2026-05-07 12:16:58 +02:00
parent dad82a79de
commit 39dd9ab9eb
99 changed files with 7442 additions and 131 deletions
+4
View File
@@ -21,6 +21,8 @@ const LoginPage = lazy(() => import('./pages/LoginPage'))
const RegisterPage = lazy(() => import('./pages/RegisterPage'))
const DashboardPage = lazy(() => import('./pages/DashboardPage'))
const ReposPage = lazy(() => import('./pages/ReposPage'))
const CreateRepoPage = lazy(() => import('./pages/CreateRepoPage'))
const ImportRepoPage = lazy(() => import('./pages/ImportRepoPage'))
const RepoPage = lazy(() => import('./pages/RepoPage'))
const BlobPage = lazy(() => import('./pages/BlobPage'))
const RepoSettingsPage = lazy(() => import('./pages/RepoSettingsPage'))
@@ -68,6 +70,8 @@ export default function App() {
<Route index element={<S><DashboardPage /></S>} />
<Route path="repos" element={<S><ReposPage /></S>} />
<Route path="repos/new" element={<S><CreateRepoPage /></S>} />
<Route path="repos/import" element={<S><ImportRepoPage /></S>} />
<Route path="repos/:owner/:repo" element={<S><RepoPage /></S>} />
<Route path="repos/:owner/:repo/blob" element={<S><BlobPage /></S>} />
<Route path="repos/:owner/:repo/commits" element={<S><CommitsPage /></S>} />
+25 -2
View File
@@ -152,8 +152,31 @@ export function useUpdateBlob(owner: string, name: string) {
export function useCreateRepo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: { name: string; description?: string; isPrivate?: boolean }) =>
api.post<Repository>('/api/v1/repos', repositorySchema, data),
mutationFn: (data: {
name: string
description?: string
isPrivate?: boolean
defaultBranch?: string
initReadme?: 'none' | 'blank' | 'tutorial'
initGitignore?: boolean
}) => api.post<Repository>('/api/v1/repos', repositorySchema, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['repos'] })
},
})
}
export function useImportRepo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: {
url: string
name: string
description?: string
isPrivate?: boolean
authUser?: string
authPass?: string
}) => api.post<Repository>('/api/v1/repos/import', repositorySchema, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['repos'] })
},
+29 -45
View File
@@ -1,7 +1,6 @@
import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { useAuth } from '../../contexts/AuthContext'
import { useCreateRepo } from '../../api/queries/repos'
export function Header() {
const { user, isAuthenticated } = useAuth()
@@ -96,52 +95,37 @@ export function Header() {
}
function CreateMenu({ onClose }: { onClose: () => void }) {
const navigate = useNavigate()
const createRepo = useCreateRepo()
const [name, setName] = useState('')
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault()
if (!name.trim()) return
const repo = await createRepo.mutateAsync({ name: name.trim() })
onClose()
navigate(`/repos/${repo.ownerName}/${repo.name}`)
}
return (
<div className="absolute right-0 top-full mt-1 w-72 bg-white rounded-lg shadow-xl border border-[#DFE1E6] z-50 overflow-hidden">
<div className="px-4 py-3 border-b border-[#DFE1E6]">
<p className="text-xs font-semibold text-[#5E6C84] uppercase tracking-wide">Quick create</p>
</div>
<form onSubmit={handleCreate} className="p-4 space-y-3">
<div>
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Repository name</label>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="my-new-repo"
autoFocus
required
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]"
/>
</div>
<button
type="submit"
disabled={createRepo.isPending || !name.trim()}
className="w-full py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] disabled:opacity-50"
>
{createRepo.isPending ? 'Creating…' : 'Create repository'}
</button>
</form>
<div className="border-t border-[#DFE1E6]">
<Link to="/repos" onClick={onClose}
className="flex items-center gap-2 px-4 py-3 text-sm text-[#172B4D] hover:bg-[#F4F5F7]">
<svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5" 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>
All repositories
</Link>
<div className="absolute right-0 top-full mt-1 w-56 bg-white rounded-lg shadow-xl border border-[#DFE1E6] z-50 overflow-hidden">
<div className="px-4 py-2.5 border-b border-[#DFE1E6]">
<p className="text-xs font-semibold text-[#5E6C84] uppercase tracking-wide">Create</p>
</div>
<ul>
<li>
<Link
to="/repos/new"
onClick={onClose}
className="flex items-center gap-2.5 px-4 py-3 text-sm text-[#172B4D] hover:bg-[#F4F5F7]"
>
<svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5" 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>
New repository
</Link>
</li>
<li>
<Link
to="/repos/import"
onClick={onClose}
className="flex items-center gap-2.5 px-4 py-3 text-sm text-[#172B4D] hover:bg-[#F4F5F7]"
>
<svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
Import repository
</Link>
</li>
</ul>
</div>
)
}
+181
View File
@@ -0,0 +1,181 @@
import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { useCreateRepo } from '../api/queries/repos'
export default function CreateRepoPage() {
const navigate = useNavigate()
const createRepo = useCreateRepo()
const [name, setName] = useState('')
const [isPrivate, setIsPrivate] = useState(true)
const [initReadme, setInitReadme] = useState<'none' | 'blank' | 'tutorial'>('none')
const [defaultBranch, setDefaultBranch] = useState('')
const [initGitignore, setInitGitignore] = useState(true)
const [showAdvanced, setShowAdvanced] = useState(false)
const [description, setDescription] = useState('')
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!name.trim()) return
const repo = await createRepo.mutateAsync({
name: name.trim(),
description,
isPrivate,
defaultBranch: defaultBranch.trim() || 'main',
initReadme,
initGitignore,
})
navigate(`/repos/${repo.ownerName}/${repo.name}`)
}
return (
<div className="max-w-2xl mx-auto px-4 py-10">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h1 className="text-xl font-semibold text-[#172B4D]">Create a new repository</h1>
<Link to="/repos/import" className="text-sm text-[#0052CC] hover:underline">
Import repository
</Link>
</div>
<div className="border-t border-[#DFE1E6] mb-6" />
<form onSubmit={handleSubmit} className="space-y-5">
{/* Repository name */}
<Field label="Repository name" required>
<input
value={name}
onChange={e => setName(e.target.value.replace(/[^a-zA-Z0-9._-]/g, '-'))}
placeholder="my-repository"
required
autoFocus
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF]"
/>
{name && !/^[a-zA-Z0-9._-]+$/.test(name) && (
<p className="text-xs text-[#DE350B] mt-1">Only letters, numbers, hyphens, underscores and dots are allowed.</p>
)}
</Field>
{/* Access level */}
<Field label="Access level">
<label className="flex items-start gap-2.5 cursor-pointer">
<input
type="checkbox"
checked={isPrivate}
onChange={e => setIsPrivate(e.target.checked)}
className="mt-0.5 w-4 h-4 accent-[#0052CC]"
/>
<div>
<span className="text-sm font-medium text-[#172B4D]">Private repository</span>
<p className="text-xs text-[#0052CC] mt-0.5 leading-relaxed">
{isPrivate
? 'Uncheck to make this repository public. Public repositories typically contain open-source code and can be viewed by anyone.'
: 'Check to make this repository private. Only invited collaborators can see and push to it.'}
</p>
</div>
</label>
</Field>
{/* Include README */}
<Field label="Include a README?">
<select
value={initReadme}
onChange={e => setInitReadme(e.target.value as 'none' | 'blank' | 'tutorial')}
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] bg-white"
>
<option value="none">No</option>
<option value="blank">Yes, blank</option>
<option value="tutorial">Yes, with a tutorial (for beginners)</option>
</select>
</Field>
{/* Default branch */}
<Field label="Default branch name">
<input
value={defaultBranch}
onChange={e => setDefaultBranch(e.target.value)}
placeholder="e.g., 'main'"
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF]"
/>
</Field>
{/* Include .gitignore */}
<Field label="Include .gitignore?">
<select
value={initGitignore ? 'yes' : 'no'}
onChange={e => setInitGitignore(e.target.value === 'yes')}
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] bg-white"
>
<option value="yes">Yes (recommended)</option>
<option value="no">No</option>
</select>
</Field>
{/* Advanced settings */}
<div>
<button
type="button"
onClick={() => setShowAdvanced(s => !s)}
className="flex items-center gap-1.5 text-sm text-[#0052CC] hover:underline font-medium"
>
<svg
width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24"
className={`transition-transform ${showAdvanced ? 'rotate-180' : ''}`}
>
<path strokeLinecap="round" strokeLinejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
Advanced settings
</button>
{showAdvanced && (
<div className="mt-4 space-y-4 border-t border-[#DFE1E6] pt-4">
<Field label="Description">
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
rows={4}
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] resize-y"
placeholder="Describe your repository…"
/>
</Field>
</div>
)}
</div>
{createRepo.isError && (
<p className="text-xs text-[#DE350B]">
{createRepo.error instanceof Error ? createRepo.error.message : 'Failed to create repository.'}
</p>
)}
{/* Footer actions */}
<div className="border-t border-[#DFE1E6] pt-5 flex items-center justify-end gap-3">
<Link
to="/repos"
className="px-4 py-2 rounded border border-[#DFE1E6] text-sm text-[#172B4D] hover:bg-[#F4F5F7] min-h-[36px] flex items-center"
>
Cancel
</Link>
<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-[36px]"
>
{createRepo.isPending ? 'Creating…' : 'Create repository'}
</button>
</div>
</form>
</div>
)
}
function Field({ label, required, children }: { label: string; required?: boolean; children: React.ReactNode }) {
return (
<div className="grid grid-cols-[180px_1fr] gap-4 items-start">
<label className="text-sm text-[#172B4D] text-right pt-2 leading-tight">
{label}{required && <span className="text-[#DE350B] ml-0.5">*</span>}
</label>
<div>{children}</div>
</div>
)
}
+206
View File
@@ -0,0 +1,206 @@
import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { useImportRepo } from '../api/queries/repos'
export default function ImportRepoPage() {
const navigate = useNavigate()
const importRepo = useImportRepo()
const [url, setUrl] = useState('')
const [requiresAuth, setRequiresAuth] = useState(false)
const [authUser, setAuthUser] = useState('')
const [authPass, setAuthPass] = useState('')
const [name, setName] = useState('')
const [isPrivate, setIsPrivate] = useState(true)
const [showAdvanced, setShowAdvanced] = useState(false)
const [description, setDescription] = useState('')
// Auto-fill name from URL
function handleUrlChange(val: string) {
setUrl(val)
if (!name) {
const segment = val.split('/').pop()?.replace(/\.git$/, '') ?? ''
if (segment) setName(segment)
}
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!url.trim() || !name.trim()) return
const repo = await importRepo.mutateAsync({
url: url.trim(),
name: name.trim(),
description,
isPrivate,
authUser: requiresAuth ? authUser : undefined,
authPass: requiresAuth ? authPass : undefined,
})
navigate(`/repos/${repo.ownerName}/${repo.name}`)
}
return (
<div className="max-w-2xl mx-auto px-4 py-10">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h1 className="text-xl font-semibold text-[#172B4D]">Import existing code</h1>
<Link to="/repos/new" className="text-sm text-[#0052CC] hover:underline">
Create new repository
</Link>
</div>
<div className="border-t border-[#DFE1E6] mb-6" />
<form onSubmit={handleSubmit} className="space-y-6">
{/* Old repository section */}
<div>
<h2 className="text-sm font-semibold text-[#172B4D] mb-4">Old repository</h2>
<div className="space-y-4">
<Field label="URL" required>
<input
value={url}
onChange={e => handleUrlChange(e.target.value)}
placeholder="https://github.com/user/repo.git"
required
autoFocus
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF]"
/>
</Field>
<Field label="">
<label className="flex items-center gap-2.5 cursor-pointer">
<input
type="checkbox"
checked={requiresAuth}
onChange={e => setRequiresAuth(e.target.checked)}
className="w-4 h-4 accent-[#0052CC]"
/>
<span className="text-sm text-[#172B4D]">Requires authorization</span>
</label>
{requiresAuth && (
<div className="mt-3 space-y-3">
<input
value={authUser}
onChange={e => setAuthUser(e.target.value)}
placeholder="Username"
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]"
/>
<input
type="password"
value={authPass}
onChange={e => setAuthPass(e.target.value)}
placeholder="Password or access token"
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]"
/>
</div>
)}
</Field>
</div>
</div>
<div className="border-t border-[#DFE1E6]" />
{/* New repository section */}
<div>
<h2 className="text-sm font-semibold text-[#172B4D] mb-4">New repository</h2>
<div className="space-y-4">
<Field label="Repository name" required>
<input
value={name}
onChange={e => setName(e.target.value.replace(/[^a-zA-Z0-9._-]/g, '-'))}
placeholder="my-repository"
required
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] focus:ring-1 focus:ring-[#4C9AFF]"
/>
</Field>
<Field label="Access level">
<label className="flex items-start gap-2.5 cursor-pointer">
<input
type="checkbox"
checked={isPrivate}
onChange={e => setIsPrivate(e.target.checked)}
className="mt-0.5 w-4 h-4 accent-[#0052CC]"
/>
<div>
<span className="text-sm font-medium text-[#172B4D]">Private repository</span>
<p className="text-xs text-[#0052CC] mt-0.5 leading-relaxed">
{isPrivate
? 'Uncheck to make this repository public. Public repositories typically contain open-source code and can be viewed by anyone.'
: 'Check to make this repository private. Only invited collaborators can see and push to it.'}
</p>
</div>
</label>
</Field>
{/* Advanced settings */}
<div>
<button
type="button"
onClick={() => setShowAdvanced(s => !s)}
className="flex items-center gap-1.5 text-sm text-[#0052CC] hover:underline font-medium"
>
<svg
width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24"
className={`transition-transform ${showAdvanced ? 'rotate-180' : ''}`}
>
<path strokeLinecap="round" strokeLinejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
Advanced settings
</button>
{showAdvanced && (
<div className="mt-4 space-y-4 border-t border-[#DFE1E6] pt-4">
<Field label="Description">
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
rows={4}
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] resize-y"
placeholder="Describe your repository…"
/>
</Field>
</div>
)}
</div>
</div>
</div>
{importRepo.isError && (
<p className="text-xs text-[#DE350B]">
{importRepo.error instanceof Error ? importRepo.error.message : 'Import failed.'}
</p>
)}
{/* Footer */}
<div className="border-t border-[#DFE1E6] pt-5 flex items-center justify-end gap-3">
<Link
to="/repos"
className="px-4 py-2 rounded border border-[#DFE1E6] text-sm text-[#172B4D] hover:bg-[#F4F5F7] min-h-[36px] flex items-center"
>
Cancel
</Link>
<button
type="submit"
disabled={importRepo.isPending || !url.trim() || !name.trim()}
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] disabled:opacity-50 min-h-[36px]"
>
{importRepo.isPending ? 'Importing…' : 'Import repository'}
</button>
</div>
</form>
</div>
)
}
function Field({ label, required, children }: { label: string; required?: boolean; children: React.ReactNode }) {
return (
<div className="grid grid-cols-[180px_1fr] gap-4 items-start">
<label className="text-sm text-[#172B4D] text-right pt-2 leading-tight">
{label}{required && <span className="text-[#DE350B] ml-0.5">*</span>}
</label>
<div>{children}</div>
</div>
)
}
+27 -75
View File
@@ -1,12 +1,10 @@
import { useState } from 'react'
import { useRepos, useCreateRepo } from '../api/queries/repos'
import { useRepos } from '../api/queries/repos'
import { RepoCard } from '../components/repos/RepoCard'
import { RepoListSkeleton } from '../ui/Skeleton'
import { Link } from 'react-router-dom'
export default function ReposPage() {
const { data: repos, isLoading, isError } = useRepos()
const [showCreate, setShowCreate] = useState(false)
return (
<div className="max-w-4xl mx-auto px-4 md:px-6 py-6">
@@ -17,27 +15,39 @@ export default function ReposPage() {
<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 className="flex items-center gap-2">
<Link
to="/repos/import"
className="flex items-center gap-1.5 px-3 py-2 rounded border border-[#DFE1E6] text-sm text-[#172B4D] hover:bg-[#F4F5F7] font-medium min-h-[36px]"
>
Import
</Link>
<Link
to="/repos/new"
className="flex items-center gap-2 px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] min-h-[36px]"
>
<svg width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
New repository
</Link>
</div>
</div>
{showCreate && <CreateRepoForm onClose={() => setShowCreate(false)} />}
{isLoading ? (
<RepoListSkeleton />
) : isError ? (
<NotSignedIn />
) : !repos?.length ? (
<div className="py-12 text-center text-sm text-[#5E6C84]">
No repositories yet.{' '}
<button onClick={() => setShowCreate(true)} className="text-[#0052CC] hover:underline">Create your first one.</button>
<div className="py-16 text-center">
<svg width="48" height="48" fill="none" stroke="#97A0AF" strokeWidth="1" viewBox="0 0 24 24" className="mx-auto mb-4">
<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>
<p className="text-sm font-medium text-[#172B4D] mb-1">No repositories yet</p>
<p className="text-xs text-[#5E6C84] mb-4">Create your first repository to get started.</p>
<Link to="/repos/new" className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF]">
Create repository
</Link>
</div>
) : (
<div className="flex flex-col gap-2">
@@ -64,61 +74,3 @@ function NotSignedIn() {
</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>
)
}