changed layout of repo settings page

This commit is contained in:
2026-05-07 12:32:07 +02:00
parent 39dd9ab9eb
commit 00aede9c91
5 changed files with 525 additions and 69 deletions
+1
View File
@@ -18,6 +18,7 @@ const repositorySchema = z.object({
ownerId: z.number(),
ownerName: z.string(),
isEmpty: z.boolean(),
size: z.number().default(0),
name: z.string(),
description: z.string(),
isPrivate: z.boolean(),
+506 -68
View File
@@ -1,92 +1,530 @@
import { useState } from 'react'
import { useParams, Link, useNavigate } from 'react-router-dom'
import { useState, useEffect } from 'react'
import { useParams, Link, useNavigate, useSearchParams } from 'react-router-dom'
import { useRepo, useUpdateRepo, useDeleteRepo } from '../api/queries/repos'
import { Skeleton } from '../ui/Skeleton'
// ─── Sidebar definition ───────────────────────────────────────────────────────
type SectionId =
| 'repository-details'
| 'repository-permissions'
| 'access-keys'
| 'access-tokens'
| 'branch-restrictions'
| 'branching-model'
| 'merge-strategies'
| 'webhooks'
| 'default-reviewers'
| 'default-description'
| 'excluded-files'
| 'git-lfs'
const SIDEBAR: { group: string; items: { id: SectionId; label: string; badge?: string }[] }[] = [
{
group: 'General',
items: [
{ id: 'repository-details', label: 'Repository details' },
{ id: 'repository-permissions', label: 'Repository permissions' },
],
},
{
group: 'Security',
items: [
{ id: 'access-keys', label: 'Access keys' },
{ id: 'access-tokens', label: 'Access tokens' },
],
},
{
group: 'Workflow',
items: [
{ id: 'branch-restrictions', label: 'Branch restrictions' },
{ id: 'branching-model', label: 'Branching model' },
{ id: 'merge-strategies', label: 'Merge strategies' },
{ id: 'webhooks', label: 'Webhooks' },
],
},
{
group: 'Pull Requests',
items: [
{ id: 'default-reviewers', label: 'Default reviewers' },
{ id: 'default-description', label: 'Default description' },
{ id: 'excluded-files', label: 'Excluded files' },
],
},
{
group: 'Features',
items: [
{ id: 'git-lfs', label: 'Git LFS' },
],
},
]
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.',
},
}
// ─── Main page ────────────────────────────────────────────────────────────────
export default function RepoSettingsPage() {
const { owner = '', repo: repoName = '' } = useParams<{ owner: string; repo: string }>()
const navigate = useNavigate()
const { data: repo } = useRepo(owner, repoName)
const updateRepo = useUpdateRepo(owner, repoName)
const deleteRepo = useDeleteRepo(owner, repoName)
const [searchParams, setSearchParams] = useSearchParams()
const [sidebarSearch, setSidebarSearch] = useState('')
const section = (searchParams.get('section') ?? 'repository-details') as SectionId
const [description, setDescription] = useState(repo?.description ?? '')
const [isPrivate, setIsPrivate] = useState(repo?.isPrivate ?? false)
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
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"
>
<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">
<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]"
/>
</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>
{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'
}`}
>
{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>
))}
</nav>
</aside>
{/* ── Main 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>
</div>
{section === 'repository-details'
? <RepositoryDetailsSection owner={owner} repo={repoName} />
: <ComingSoon sectionId={section} />
}
</main>
</div>
)
}
// ─── Repository details section ───────────────────────────────────────────────
function formatSize(bytes: number): string {
if (bytes === 0) return '0 B'
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) 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 { data: repoData, isLoading } = useRepo(owner, repo)
const updateRepo = useUpdateRepo(owner, repo)
const deleteRepo = useDeleteRepo(owner, repo)
const navigate = useNavigate()
const [description, setDescription] = useState('')
const [isPrivate, setIsPrivate] = useState(false)
const [defaultBranch, setDefaultBranch] = useState('')
const [showAdvanced, setShowAdvanced] = useState(false)
const [confirmDelete, setConfirmDelete] = useState('')
const [saved, setSaved] = useState(false)
const handleSave = async (e: React.FormEvent) => {
e.preventDefault()
await updateRepo.mutateAsync({ description, isPrivate })
setSaved(true)
setTimeout(() => setSaved(false), 2000)
useEffect(() => {
if (repoData) {
setDescription(repoData.description ?? '')
setIsPrivate(repoData.isPrivate)
setDefaultBranch(repoData.defaultBranch)
}
}, [repoData])
const isDirty = repoData != null && (
description !== (repoData.description ?? '') ||
isPrivate !== repoData.isPrivate ||
defaultBranch !== repoData.defaultBranch
)
function handleDiscard() {
if (!repoData) return
setDescription(repoData.description ?? '')
setIsPrivate(repoData.isPrivate)
setDefaultBranch(repoData.defaultBranch)
}
const handleDelete = async () => {
if (confirmDelete !== repoName) return
async function handleSave(e: React.FormEvent) {
e.preventDefault()
await updateRepo.mutateAsync({ description, isPrivate, defaultBranch })
setSaved(true)
setTimeout(() => setSaved(false), 3000)
}
async function handleDelete() {
if (confirmDelete !== repo || !repoData) return
await deleteRepo.mutateAsync()
navigate('/repos')
}
return (
<div className="max-w-2xl mx-auto px-4 md:px-6 py-6 space-y-8">
<div className="flex items-center gap-1 text-sm">
<Link to={`/repos/${owner}/${repoName}`} className="text-[#0052CC] hover:underline">{repoName}</Link>
<span className="text-[#5E6C84]">/</span>
<span className="font-semibold text-[#172B4D]">Settings</span>
if (isLoading || !repoData) {
return (
<div className="max-w-2xl px-6 py-6 space-y-4">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-5 w-full" />
<Skeleton className="h-24 w-full" />
</div>
)
}
<h1 className="text-xl font-semibold text-[#172B4D]">Repository Settings</h1>
// Initials avatar colour based on repo name
const avatarColor = '#0052CC'
<section className="border border-[#DFE1E6] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#DFE1E6] bg-[#FAFBFC]">
<h2 className="text-sm font-semibold text-[#172B4D]">General</h2>
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>
<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>
<span>/</span>
<Link to={`/repos/${owner}/${repo}`} className="hover:text-[#0052CC]">{repo}</Link>
</div>
<h1 className="text-xl font-semibold text-[#172B4D]">Repository details</h1>
</div>
<form onSubmit={handleSave} className="px-5 py-5 space-y-4">
<div>
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Repository name</label>
<input value={repoName} disabled
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm bg-[#F4F5F7] text-[#5E6C84] cursor-not-allowed" />
<p className="text-xs text-[#5E6C84] mt-1">Renaming requires migrating git remotes coming soon.</p>
</div>
<div>
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Description</label>
<input value={description} onChange={e => setDescription(e.target.value)}
placeholder="Short description of this repository"
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 repository</span>
</label>
<div className="flex items-center gap-3">
<button type="submit" disabled={updateRepo.isPending}
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] disabled:opacity-50 min-h-[44px]">
{updateRepo.isPending ? 'Saving…' : 'Save changes'}
</button>
{saved && <span className="text-xs text-[#00875A] font-medium">Saved!</span>}
</div>
</form>
</section>
<section className="border border-[#FFEBE6] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#FFEBE6] bg-[#FFEBE6]/50">
<h2 className="text-sm font-semibold text-[#BF2600]">Danger zone</h2>
</div>
<div className="px-5 py-5 space-y-3">
<p className="text-sm text-[#172B4D] font-medium">Delete this repository</p>
<p className="text-xs text-[#5E6C84]">
This action is permanent. Type <code className="font-mono bg-[#F4F5F7] px-1 rounded">{repoName}</code> to confirm.
</p>
<input value={confirmDelete} onChange={e => setConfirmDelete(e.target.value)}
placeholder={repoName}
className="w-full border border-[#DE350B] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#DE350B]" />
<button onClick={handleDelete}
disabled={confirmDelete !== repoName || deleteRepo.isPending}
className="px-4 py-2 rounded bg-[#DE350B] text-white text-sm font-medium hover:bg-[#BF2600] disabled:opacity-40 min-h-[44px]">
{deleteRepo.isPending ? 'Deleting…' : 'Delete repository'}
<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
<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>
</div>
</section>
</div>
<div className="border-t border-[#DFE1E6]" />
<form onSubmit={handleSave} className="space-y-6">
{/* Avatar */}
<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>
<button
type="button"
className="px-3 py-1.5 text-sm border border-[#DFE1E6] rounded text-[#172B4D] hover:bg-[#F4F5F7]"
>
Change avatar
</button>
</div>
</div>
{/* Repository name (read-only) */}
<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"
/>
<p className="text-xs text-[#5E6C84] mt-1">
Renaming would require all collaborators to update their git remotes. Coming soon.
</p>
</div>
{/* Size (read-only) */}
<div>
<label className="block text-sm font-medium text-[#172B4D] mb-1">Size</label>
<p className="text-sm text-[#172B4D]">{formatSize(repoData.size)}</p>
</div>
{/* Description */}
<div>
<label className="block text-sm font-medium text-[#172B4D] mb-1">Description</label>
<textarea
value={description}
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"
/>
</div>
{/* Access level */}
<div>
<label className="block text-sm font-medium text-[#172B4D] mb-2">Access level</label>
<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 text-[#172B4D]">
{isPrivate ? '✓ This is a private repository' : 'Make this repository private'}
</span>
<p className="text-xs text-[#5E6C84] mt-0.5">
{isPrivate
? 'Only collaborators you invite can see and push to this repository.'
: 'Anyone can view this repository. Make it private to restrict access.'}
</p>
</div>
</label>
</div>
{/* Advanced (collapsible) */}
<div className="border border-[#DFE1E6] rounded-lg overflow-hidden">
<button
type="button"
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' : ''}`}
>
<path strokeLinecap="round" strokeLinejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
Advanced
</button>
{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"
/>
<p className="text-xs text-[#5E6C84] mt-1">Used as the base branch 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]">
<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.
</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.
</p>
<input
value={confirmDelete}
onChange={e => setConfirmDelete(e.target.value)}
placeholder={repo}
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#DE350B]"
/>
<button
type="button"
onClick={handleDelete}
disabled={confirmDelete !== repo || deleteRepo.isPending}
className="px-4 py-2 rounded bg-[#DE350B] text-white text-sm font-medium hover:bg-[#BF2600] disabled:opacity-40"
>
{deleteRepo.isPending ? 'Deleting…' : 'Delete repository'}
</button>
{deleteRepo.isError && (
<p className="text-xs text-[#DE350B]">{(deleteRepo.error as Error).message}</p>
)}
</div>
</div>
</div>
)}
</div>
{/* Footer */}
<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 && (
<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" />
</svg>
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]"
>
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]"
>
{updateRepo.isPending ? 'Saving…' : 'Save changes'}
</button>
</div>
</form>
</div>
)
}
// ─── 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>
),
}
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>
<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>
</div>
</div>
)
}
+1
View File
@@ -20,6 +20,7 @@ export interface Repository {
isPrivate: boolean
defaultBranch: string
isEmpty: boolean
size: number
createdAt: string
updatedAt: string
}