repo details page mostly working

This commit is contained in:
2026-05-07 13:04:13 +02:00
parent 00aede9c91
commit 12bcf59bc9
12 changed files with 407 additions and 214 deletions
+179 -192
View File
@@ -1,9 +1,10 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef, useCallback } from 'react'
import { useParams, Link, useNavigate, useSearchParams } from 'react-router-dom'
import { useRepo, useUpdateRepo, useDeleteRepo } from '../api/queries/repos'
import { useRepo, useUpdateRepo, useDeleteRepo, useUploadRepoAvatar } from '../api/queries/repos'
import { Skeleton } from '../ui/Skeleton'
import { RepoAvatar } from '../ui/RepoAvatar'
// ─── Sidebar definition ───────────────────────────────────────────────────────
// ─── Sidebar config ───────────────────────────────────────────────────────────
type SectionId =
| 'repository-details'
@@ -19,7 +20,7 @@ type SectionId =
| 'excluded-files'
| 'git-lfs'
const SIDEBAR: { group: string; items: { id: SectionId; label: string; badge?: string }[] }[] = [
const SIDEBAR: { group: string; items: { id: SectionId; label: string }[] }[] = [
{
group: 'General',
items: [
@@ -61,53 +62,20 @@ const SIDEBAR: { group: string; items: { id: SectionId; label: string; badge?: s
const SECTION_META: Record<SectionId, { title: string; description: string }> = {
'repository-details': { title: 'Repository details', description: '' },
'repository-permissions': {
title: 'Repository permissions',
description: 'Control who can read, write, or administer this repository. Invite collaborators and manage team access.',
},
'access-keys': {
title: 'Access keys',
description: 'SSH deploy keys give read or write access to this repository without requiring an interactive user account. Useful for CI/CD systems.',
},
'access-tokens': {
title: 'Access tokens',
description: 'Repository access tokens give third-party tools secure, scoped access to this repository via the API.',
},
'branch-restrictions': {
title: 'Branch restrictions',
description: 'Protect important branches by restricting who can push, delete, or force-push to them. Define which users or groups can merge pull requests.',
},
'branching-model': {
title: 'Branching model',
description: 'Define a branching strategy (e.g. Gitflow) to enforce consistent branch naming across your team. Automatically categorise branches.',
},
'merge-strategies': {
title: 'Merge strategies',
description: 'Configure which merge strategies (merge commit, squash, rebase) are allowed when closing pull requests.',
},
'webhooks': {
title: 'Webhooks',
description: 'Send real-time HTTP POST notifications to external services when events occur in this repository — push, PR creation, comments, and more.',
},
'default-reviewers': {
title: 'Default reviewers',
description: 'Automatically add reviewers to pull requests based on the files changed or the target branch.',
},
'default-description': {
title: 'Default description',
description: 'Set a default description template that pre-fills the body field when a pull request is created.',
},
'excluded-files': {
title: 'Excluded files',
description: 'Specify files that should be excluded from pull request diff views and review requirements.',
},
'git-lfs': {
title: 'Git LFS',
description: 'Git Large File Storage replaces large files with text pointers inside git, while storing the actual files on a remote server.',
},
'repository-permissions': { title: 'Repository permissions', description: 'Control who can read, write, or administer this repository. Invite collaborators and manage team access.' },
'access-keys': { title: 'Access keys', description: 'SSH deploy keys give read or write access to this repository without requiring an interactive user account. Useful for CI/CD.' },
'access-tokens': { title: 'Access tokens', description: 'Repository access tokens give third-party tools secure, scoped access to this repository via the API.' },
'branch-restrictions': { title: 'Branch restrictions', description: 'Protect important branches by restricting who can push, delete, or force-push to them.' },
'branching-model': { title: 'Branching model', description: 'Define a branching strategy (e.g. Gitflow) to enforce consistent branch naming across your team.' },
'merge-strategies': { title: 'Merge strategies', description: 'Configure which merge strategies (merge commit, squash, rebase) are allowed for pull requests.' },
'webhooks': { title: 'Webhooks', description: 'Send real-time HTTP POST notifications to external services when events occur in this repository.' },
'default-reviewers': { title: 'Default reviewers', description: 'Automatically add reviewers to pull requests based on changed files or the target branch.' },
'default-description': { title: 'Default description', description: 'Set a template that pre-fills the pull request body when a new PR is created.' },
'excluded-files': { title: 'Excluded files', description: 'Specify files that should be excluded from pull request diff views and review requirements.' },
'git-lfs': { title: 'Git LFS', description: 'Git Large File Storage replaces large files with text pointers inside git, while storing the actual files on a remote server.' },
}
// ─── Main page ────────────────────────────────────────────────────────────────
// ─── Page shell ───────────────────────────────────────────────────────────────
export default function RepoSettingsPage() {
const { owner = '', repo: repoName = '' } = useParams<{ owner: string; repo: string }>()
@@ -115,69 +83,36 @@ export default function RepoSettingsPage() {
const [sidebarSearch, setSidebarSearch] = useState('')
const section = (searchParams.get('section') ?? 'repository-details') as SectionId
function goTo(id: SectionId) {
setSearchParams({ section: id })
}
const filtered = sidebarSearch.trim()
? SIDEBAR.map(g => ({
...g,
items: g.items.filter(i => i.label.toLowerCase().includes(sidebarSearch.toLowerCase())),
})).filter(g => g.items.length > 0)
? SIDEBAR.map(g => ({ ...g, items: g.items.filter(i => i.label.toLowerCase().includes(sidebarSearch.toLowerCase())) })).filter(g => g.items.length > 0)
: SIDEBAR
return (
<div className="flex h-full overflow-hidden">
{/* ── Left sidebar ── */}
<aside className="w-60 shrink-0 border-r border-[#DFE1E6] bg-white hidden md:flex flex-col overflow-hidden">
{/* Top: back + search */}
<div className="p-3 border-b border-[#DFE1E6] shrink-0 space-y-2.5">
<Link
to={`/repos/${owner}/${repoName}`}
className="flex items-center gap-1.5 text-sm text-[#172B4D] hover:text-[#0052CC] font-medium"
>
{/* Sidebar */}
<aside className="w-60 shrink-0 border-r border-[#DFE1E6] bg-white hidden md:flex flex-col">
<div className="p-3 border-b border-[#DFE1E6] space-y-2.5 shrink-0">
<Link to={`/repos/${owner}/${repoName}`} className="flex items-center gap-1.5 text-sm text-[#172B4D] hover:text-[#0052CC] font-medium">
<svg width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>
Repository settings
</Link>
<div className="relative">
<svg className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[#5E6C84] pointer-events-none"
width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<svg className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[#5E6C84] pointer-events-none" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>
<input
value={sidebarSearch}
onChange={e => setSidebarSearch(e.target.value)}
placeholder="Jump to settings…"
className="w-full text-xs bg-[#F4F5F7] border border-[#DFE1E6] rounded pl-7 pr-3 py-1.5 focus:outline-none focus:border-[#4C9AFF]"
/>
<input value={sidebarSearch} onChange={e => setSidebarSearch(e.target.value)} placeholder="Jump to settings…" className="w-full text-xs bg-[#F4F5F7] border border-[#DFE1E6] rounded pl-7 pr-3 py-1.5 focus:outline-none focus:border-[#4C9AFF]" />
</div>
</div>
{/* Nav items */}
<nav className="flex-1 overflow-y-auto py-2">
{filtered.map(group => (
<div key={group.group} className="mb-1">
<p className="text-[10px] font-semibold text-[#5E6C84] uppercase tracking-wider px-4 py-1.5">
{group.group}
</p>
<p className="text-[10px] font-semibold text-[#5E6C84] uppercase tracking-wider px-4 py-1.5">{group.group}</p>
{group.items.map(item => (
<button
key={item.id}
onClick={() => goTo(item.id)}
className={`w-full text-left px-4 py-2 text-sm transition-colors flex items-center justify-between ${
section === item.id
? 'bg-[#DEEBFF] text-[#0052CC] font-medium border-l-[3px] border-[#0052CC]'
: 'text-[#172B4D] hover:bg-[#F4F5F7] border-l-[3px] border-transparent'
}`}
>
<button key={item.id} onClick={() => setSearchParams({ section: item.id })}
className={`w-full text-left px-4 py-2 text-sm transition-colors border-l-[3px] ${section === item.id ? 'bg-[#DEEBFF] text-[#0052CC] font-medium border-[#0052CC]' : 'text-[#172B4D] hover:bg-[#F4F5F7] border-transparent'}`}>
{item.label}
{item.badge && (
<span className="text-[9px] font-bold px-1.5 py-0.5 rounded bg-[#DFE1E6] text-[#5E6C84]">
{item.badge}
</span>
)}
</button>
))}
</div>
@@ -185,23 +120,13 @@ export default function RepoSettingsPage() {
</nav>
</aside>
{/* ── Main content ── */}
{/* Content */}
<main className="flex-1 overflow-y-auto min-w-0">
{/* Mobile section selector */}
<div className="md:hidden p-3 border-b border-[#DFE1E6] bg-white">
<select
value={section}
onChange={e => goTo(e.target.value as SectionId)}
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none"
>
{SIDEBAR.map(g =>
g.items.map(i => (
<option key={i.id} value={i.id}>{g.group} {i.label}</option>
))
)}
<select value={section} onChange={e => setSearchParams({ section: e.target.value })} className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none">
{SIDEBAR.map(g => g.items.map(i => <option key={i.id} value={i.id}>{g.group} {i.label}</option>))}
</select>
</div>
{section === 'repository-details'
? <RepositoryDetailsSection owner={owner} repo={repoName} />
: <ComingSoon sectionId={section} />
@@ -211,38 +136,60 @@ export default function RepoSettingsPage() {
)
}
// ─── Repository details section ───────────────────────────────────────────────
// ─── Repository details ───────────────────────────────────────────────────────
function formatSize(bytes: number): string {
if (bytes === 0) return '0 B'
if (!bytes) return ''
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(1)} KB`
if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(1)} MB`
return `${(bytes / 1024 ** 3).toFixed(2)} GB`
}
function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string }) {
const navigate = useNavigate()
const { data: repoData, isLoading } = useRepo(owner, repo)
const updateRepo = useUpdateRepo(owner, repo)
const deleteRepo = useDeleteRepo(owner, repo)
const navigate = useNavigate()
const uploadAvatar = useUploadRepoAvatar(owner, repo)
// Form state
const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [isPrivate, setIsPrivate] = useState(false)
const [defaultBranch, setDefaultBranch] = useState('')
const [showAdvanced, setShowAdvanced] = useState(false)
const [showManage, setShowManage] = useState(false)
const [confirmDelete, setConfirmDelete] = useState('')
const [saved, setSaved] = useState(false)
const [nameError, setNameError] = useState('')
// Avatar state
const fileInputRef = useRef<HTMLInputElement>(null)
const [avatarPreview, setAvatarPreview] = useState<string | null>(null)
const manageRef = useRef<HTMLDivElement>(null)
// Initialize form from API data
useEffect(() => {
if (repoData) {
setName(repoData.name)
setDescription(repoData.description ?? '')
setIsPrivate(repoData.isPrivate)
setDefaultBranch(repoData.defaultBranch)
}
}, [repoData])
// Close manage dropdown on outside click
useEffect(() => {
const handler = (e: MouseEvent) => {
if (manageRef.current && !manageRef.current.contains(e.target as Node)) setShowManage(false)
}
document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [])
const isDirty = repoData != null && (
name !== repoData.name ||
description !== (repoData.description ?? '') ||
isPrivate !== repoData.isPrivate ||
defaultBranch !== repoData.defaultBranch
@@ -250,24 +197,50 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
function handleDiscard() {
if (!repoData) return
setName(repoData.name)
setDescription(repoData.description ?? '')
setIsPrivate(repoData.isPrivate)
setDefaultBranch(repoData.defaultBranch)
setNameError('')
}
function handleNameChange(val: string) {
setName(val)
if (val && !/^[a-zA-Z0-9._-]+$/.test(val)) {
setNameError('Only letters, numbers, hyphens, underscores, and dots are allowed.')
} else {
setNameError('')
}
}
async function handleSave(e: React.FormEvent) {
e.preventDefault()
await updateRepo.mutateAsync({ description, isPrivate, defaultBranch })
setSaved(true)
setTimeout(() => setSaved(false), 3000)
if (nameError) return
const payload: Record<string, unknown> = { description, isPrivate, defaultBranch }
if (name !== repoData?.name) payload.name = name
const updated = await updateRepo.mutateAsync(payload)
if (updated.name !== repo) {
// Renamed — navigate to new URL
navigate(`/repos/${owner}/${updated.name}/settings?section=repository-details`, { replace: true })
} else {
setSaved(true)
setTimeout(() => setSaved(false), 3000)
}
}
async function handleDelete() {
if (confirmDelete !== repo || !repoData) return
if (confirmDelete !== repo) return
await deleteRepo.mutateAsync()
navigate('/repos')
}
const handleAvatarFile = useCallback((file: File) => {
const reader = new FileReader()
reader.onload = () => setAvatarPreview(reader.result as string)
reader.readAsDataURL(file)
uploadAvatar.mutate(file)
}, [uploadAvatar])
if (isLoading || !repoData) {
return (
<div className="max-w-2xl px-6 py-6 space-y-4">
@@ -278,14 +251,11 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
)
}
// Initials avatar colour based on repo name
const avatarColor = '#0052CC'
return (
<div className="max-w-2xl px-6 py-6 space-y-6">
{/* Page header */}
<div className="flex items-start justify-between gap-4">
<div className="flex items-start justify-between gap-4 flex-wrap">
<div>
<div className="flex items-center gap-1 text-xs text-[#5E6C84] mb-1.5">
<Link to={`/repos/${owner}/${repo}`} className="hover:text-[#0052CC]">{owner}</Link>
@@ -295,13 +265,30 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
<h1 className="text-xl font-semibold text-[#172B4D]">Repository details</h1>
</div>
<div className="relative shrink-0">
<button className="flex items-center gap-1.5 px-3 py-1.5 border border-[#DFE1E6] rounded text-sm text-[#172B4D] hover:bg-[#F4F5F7] font-medium">
{/* Manage repository dropdown */}
<div className="relative shrink-0" ref={manageRef}>
<button
onClick={() => setShowManage(s => !s)}
className="flex items-center gap-1.5 px-3 py-1.5 border border-[#DFE1E6] rounded text-sm text-[#172B4D] hover:bg-[#F4F5F7] font-medium"
>
Manage repository
<svg width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</button>
{showManage && (
<div className="absolute right-0 top-full mt-1 w-52 bg-white border border-[#DFE1E6] rounded-lg shadow-xl z-20 overflow-hidden">
<button
onClick={() => { setShowManage(false); setShowAdvanced(true); setTimeout(() => document.getElementById('danger-zone')?.scrollIntoView({ behavior: 'smooth' }), 50) }}
className="w-full flex items-center gap-2 px-4 py-3 text-sm text-[#DE350B] hover:bg-[#FFEBE6] text-left"
>
<svg width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
Delete repository
</button>
</div>
)}
</div>
</div>
@@ -313,34 +300,70 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
<div>
<label className="block text-sm font-medium text-[#172B4D] mb-3">Avatar</label>
<div className="flex items-center gap-4">
<div
className="w-12 h-12 rounded-lg flex items-center justify-center text-white text-xl font-bold"
style={{ backgroundColor: avatarColor }}
>
{repoData.name[0]?.toUpperCase()}
</div>
{/* Clickable avatar */}
<button
type="button"
className="px-3 py-1.5 text-sm border border-[#DFE1E6] rounded text-[#172B4D] hover:bg-[#F4F5F7]"
onClick={() => fileInputRef.current?.click()}
className="rounded-lg overflow-hidden ring-2 ring-[#DFE1E6] hover:ring-[#4C9AFF] transition-all focus:outline-none"
title="Click to change avatar"
>
Change avatar
{avatarPreview ? (
<img src={avatarPreview} alt="Preview" className="w-14 h-14 object-cover" />
) : (
<RepoAvatar
ownerName={owner}
name={repoData.name}
avatarUrl={repoData.avatarUrl}
size={56}
/>
)}
</button>
<div>
<button
type="button"
onClick={() => fileInputRef.current?.click()}
disabled={uploadAvatar.isPending}
className="px-3 py-1.5 text-sm border border-[#DFE1E6] rounded text-[#172B4D] hover:bg-[#F4F5F7] disabled:opacity-50"
>
{uploadAvatar.isPending ? 'Uploading…' : 'Change avatar'}
</button>
<p className="text-xs text-[#5E6C84] mt-1">JPEG, PNG, GIF or WebP · max 5 MB</p>
{uploadAvatar.isError && (
<p className="text-xs text-[#DE350B] mt-1">{(uploadAvatar.error as Error).message}</p>
)}
{uploadAvatar.isSuccess && !avatarPreview && (
<p className="text-xs text-[#00875A] mt-1">Avatar updated.</p>
)}
</div>
<input
ref={fileInputRef}
type="file"
accept="image/jpeg,image/png,image/gif,image/webp"
className="hidden"
onChange={e => { const f = e.target.files?.[0]; if (f) handleAvatarFile(f) }}
/>
</div>
</div>
{/* Repository name (read-only) */}
{/* Repository name */}
<div>
<label className="block text-sm font-medium text-[#172B4D] mb-1">
Repository name <span className="text-[#DE350B]">*</span>
</label>
<input
value={repoData.name}
disabled
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm bg-[#F4F5F7] text-[#5E6C84] cursor-not-allowed"
value={name}
onChange={e => handleNameChange(e.target.value)}
className={`w-full border rounded px-3 py-2 text-sm focus:outline-none focus:ring-1 ${nameError ? 'border-[#DE350B] focus:border-[#DE350B] focus:ring-[#DE350B]' : 'border-[#DFE1E6] focus:border-[#4C9AFF] focus:ring-[#4C9AFF]'}`}
/>
<p className="text-xs text-[#5E6C84] mt-1">
Renaming would require all collaborators to update their git remotes. Coming soon.
</p>
{nameError
? <p className="text-xs text-[#DE350B] mt-1">{nameError}</p>
: name !== repoData.name
? <p className="text-xs text-[#FF8B00] mt-1 flex items-center gap-1">
<svg width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /></svg>
Renaming will change the clone URL all existing git remotes will need to be updated.
</p>
: <p className="text-xs text-[#5E6C84] mt-1">Letters, numbers, hyphens, underscores, and dots only.</p>
}
</div>
{/* Size (read-only) */}
@@ -357,7 +380,7 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
onChange={e => setDescription(e.target.value)}
rows={4}
placeholder="Describe this repository…"
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] resize-y"
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] resize-y"
/>
</div>
@@ -372,8 +395,8 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
className="mt-0.5 w-4 h-4 accent-[#0052CC]"
/>
<div>
<span className="text-sm text-[#172B4D]">
{isPrivate ? 'This is a private repository' : 'Make this repository private'}
<span className="text-sm font-medium text-[#172B4D]">
{isPrivate ? 'This is a private repository' : 'This is a public repository'}
</span>
<p className="text-xs text-[#5E6C84] mt-0.5">
{isPrivate
@@ -391,10 +414,8 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
onClick={() => setShowAdvanced(s => !s)}
className="w-full flex items-center gap-2 px-4 py-3 text-sm font-medium text-[#172B4D] hover:bg-[#F4F5F7] text-left"
>
<svg
width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24"
className={`transition-transform shrink-0 ${showAdvanced ? 'rotate-90' : ''}`}
>
<svg width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24"
className={`transition-transform shrink-0 ${showAdvanced ? 'rotate-90' : ''}`}>
<path strokeLinecap="round" strokeLinejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
Advanced
@@ -402,30 +423,29 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
{showAdvanced && (
<div className="border-t border-[#DFE1E6] px-4 py-5 space-y-6 bg-[#FAFBFC]">
{/* Default branch */}
<div>
<label className="block text-sm font-medium text-[#172B4D] mb-1">Default branch</label>
<input
value={defaultBranch}
onChange={e => setDefaultBranch(e.target.value)}
className="w-full max-w-xs border border-[#DFE1E6] bg-white rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]"
placeholder="main"
className="w-full max-w-xs bg-white border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]"
/>
<p className="text-xs text-[#5E6C84] mt-1">Used as the base branch for new pull requests.</p>
<p className="text-xs text-[#5E6C84] mt-1">The default branch used as the base for new pull requests.</p>
</div>
{/* Danger zone */}
<div className="border border-[#FFEBE6] rounded-lg overflow-hidden">
<div className="px-4 py-3 bg-[#FFEBE6]/50 border-b border-[#FFEBE6]">
<div id="danger-zone" className="border border-[#FFEBE6] rounded-lg overflow-hidden">
<div className="px-4 py-3 bg-[#FFEBE6]/60 border-b border-[#FFEBE6]">
<h3 className="text-sm font-semibold text-[#BF2600]">Delete repository</h3>
</div>
<div className="px-4 py-4 space-y-3 bg-white">
<p className="text-sm text-[#172B4D]">
This is permanent and cannot be undone. All commits, branches, pull requests, issues, and settings will be lost.
This is permanent and cannot be undone. All commits, branches, pull requests, issues, and settings will be permanently deleted.
</p>
<p className="text-xs text-[#5E6C84]">
Type <code className="font-mono bg-[#F4F5F7] border border-[#DFE1E6] px-1.5 py-0.5 rounded">{repo}</code> to confirm.
Type <code className="font-mono bg-[#F4F5F7] border border-[#DFE1E6] px-1.5 py-0.5 rounded text-[#172B4D]">{repo}</code> to confirm.
</p>
<input
value={confirmDelete}
@@ -450,12 +470,12 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
)}
</div>
{/* Footer */}
{/* Footer: error / saved / Discard / Save */}
<div className="flex items-center justify-end gap-3 pt-2 border-t border-[#DFE1E6]">
{updateRepo.isError && (
<p className="text-xs text-[#DE350B] mr-auto">{(updateRepo.error as Error).message}</p>
)}
{saved && (
{saved && !updateRepo.isError && (
<span className="text-xs text-[#00875A] font-medium mr-auto flex items-center gap-1">
<svg width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
@@ -463,19 +483,12 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
Changes saved
</span>
)}
<button
type="button"
onClick={handleDiscard}
disabled={!isDirty}
className="px-4 py-2 rounded border border-[#DFE1E6] text-sm text-[#172B4D] hover:bg-[#F4F5F7] disabled:opacity-40 min-h-[36px]"
>
<button type="button" onClick={handleDiscard} disabled={!isDirty}
className="px-4 py-2 rounded border border-[#DFE1E6] text-sm text-[#172B4D] hover:bg-[#F4F5F7] disabled:opacity-40 min-h-[36px]">
Discard
</button>
<button
type="submit"
disabled={updateRepo.isPending || !isDirty}
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] disabled:opacity-50 min-h-[36px]"
>
<button type="submit" disabled={updateRepo.isPending || !isDirty || !!nameError}
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] disabled:opacity-50 min-h-[36px]">
{updateRepo.isPending ? 'Saving…' : 'Save changes'}
</button>
</div>
@@ -484,46 +497,20 @@ function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string
)
}
// ─── Coming soon section ──────────────────────────────────────────────────────
const COMING_SOON_ICONS: Partial<Record<SectionId, React.ReactNode>> = {
'access-keys': (
<svg width="40" height="40" fill="none" stroke="#97A0AF" strokeWidth="1.2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 0 1 21.75 8.25Z" />
</svg>
),
'webhooks': (
<svg width="40" height="40" fill="none" stroke="#97A0AF" strokeWidth="1.2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />
</svg>
),
'branch-restrictions': (
<svg width="40" height="40" fill="none" stroke="#97A0AF" strokeWidth="1.2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z" />
</svg>
),
}
// ─── Coming soon ──────────────────────────────────────────────────────────────
function ComingSoon({ sectionId }: { sectionId: SectionId }) {
const meta = SECTION_META[sectionId]
return (
<div className="max-w-2xl px-6 py-6">
<h1 className="text-xl font-semibold text-[#172B4D] mb-1">{meta.title}</h1>
<div className="mt-8 flex flex-col items-center justify-center py-16 border border-dashed border-[#DFE1E6] rounded-lg text-center bg-[#FAFBFC]">
<div className="mb-4 text-[#97A0AF]">
{COMING_SOON_ICONS[sectionId] ?? (
<svg width="40" height="40" fill="none" stroke="#97A0AF" strokeWidth="1.2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75" />
</svg>
)}
</div>
<svg width="40" height="40" fill="none" stroke="#97A0AF" strokeWidth="1.2" viewBox="0 0 24 24" className="mb-4">
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75" />
</svg>
<h2 className="text-base font-semibold text-[#172B4D] mb-2">{meta.title}</h2>
<p className="text-sm text-[#5E6C84] max-w-sm leading-relaxed">{meta.description}</p>
<span className="mt-5 inline-block text-[10px] font-semibold uppercase tracking-wider text-white bg-[#97A0AF] px-2.5 py-1 rounded-full">
Coming soon
</span>
<span className="mt-5 text-[10px] font-semibold uppercase tracking-wider text-white bg-[#97A0AF] px-2.5 py-1 rounded-full">Coming soon</span>
</div>
</div>
)