phase 3 initial completion
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useRepoTree } from '../../api/queries/repos'
|
||||
import { Skeleton } from '../../ui/Skeleton'
|
||||
import { cn } from '../../lib/utils'
|
||||
|
||||
interface TreeBrowserProps {
|
||||
owner: string
|
||||
repo: string
|
||||
ref: string
|
||||
path?: string
|
||||
}
|
||||
|
||||
export function TreeBrowser({ owner, repo, ref, path = '' }: TreeBrowserProps) {
|
||||
const { data: entries, isLoading, isError } = useRepoTree(owner, repo, ref, path)
|
||||
|
||||
if (isLoading) return <TreeSkeleton />
|
||||
if (isError) return <p className="text-xs text-[#DE350B] p-4">Failed to load file tree.</p>
|
||||
if (!entries?.length) return <p className="text-xs text-[#5E6C84] p-4">Empty repository.</p>
|
||||
|
||||
const dirs = entries.filter(e => e.type === 'tree').sort((a, b) => a.name.localeCompare(b.name))
|
||||
const files = entries.filter(e => e.type === 'blob').sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
return (
|
||||
<div className="border border-[#DFE1E6] rounded overflow-hidden">
|
||||
{/* Breadcrumb */}
|
||||
{path && (
|
||||
<div className="flex items-center gap-1 px-3 py-2 bg-[#F4F5F7] border-b border-[#DFE1E6] text-xs text-[#5E6C84]">
|
||||
<Link to={`/repos/${owner}/${repo}`} className="hover:text-[#0052CC]">{repo}</Link>
|
||||
{path.split('/').map((seg, i, arr) => {
|
||||
const partial = arr.slice(0, i + 1).join('/')
|
||||
return (
|
||||
<span key={partial} className="flex items-center gap-1">
|
||||
<span>/</span>
|
||||
{i < arr.length - 1
|
||||
? <Link to={`/repos/${owner}/${repo}?path=${partial}`} className="hover:text-[#0052CC]">{seg}</Link>
|
||||
: <span className="text-[#172B4D] font-medium">{seg}</span>
|
||||
}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Entries */}
|
||||
<ul>
|
||||
{[...dirs, ...files].map((entry, i) => {
|
||||
const entryPath = path ? `${path}/${entry.name}` : entry.name
|
||||
const isDir = entry.type === 'tree'
|
||||
const href = isDir
|
||||
? `/repos/${owner}/${repo}?path=${entryPath}`
|
||||
: `/repos/${owner}/${repo}/blob?ref=${ref}&path=${entryPath}`
|
||||
|
||||
return (
|
||||
<li
|
||||
key={entry.hash}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2 hover:bg-[#FAFBFC] text-sm border-b border-[#DFE1E6] last:border-b-0',
|
||||
i === 0 && !path && 'border-t-0',
|
||||
)}
|
||||
>
|
||||
{isDir ? (
|
||||
<svg width="16" height="16" fill="#0052CC" viewBox="0 0 24 24" className="shrink-0">
|
||||
<path 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>
|
||||
) : (
|
||||
<svg width="16" height="16" fill="none" stroke="#5E6C84" strokeWidth="1.5" viewBox="0 0 24 24" className="shrink-0">
|
||||
<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>
|
||||
)}
|
||||
<Link
|
||||
to={href}
|
||||
className={cn(
|
||||
'flex-1 truncate',
|
||||
isDir ? 'text-[#0052CC] hover:underline' : 'text-[#172B4D] hover:text-[#0052CC]',
|
||||
)}
|
||||
>
|
||||
{entry.name}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TreeSkeleton() {
|
||||
return (
|
||||
<div className="border border-[#DFE1E6] rounded overflow-hidden">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-3 px-3 py-2 border-b border-[#DFE1E6] last:border-b-0">
|
||||
<Skeleton className="w-4 h-4 shrink-0" />
|
||||
<Skeleton className={`h-4 ${i % 2 === 0 ? 'w-32' : 'w-48'}`} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Collapsible row used in inline tree navigation
|
||||
export function TreeRow({
|
||||
name,
|
||||
type,
|
||||
href,
|
||||
}: {
|
||||
name: string
|
||||
type: 'blob' | 'tree'
|
||||
href: string
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<div>
|
||||
<Link to={href} className="flex items-center gap-2 py-1 hover:text-[#0052CC] text-sm">
|
||||
{type === 'tree' && (
|
||||
<button onClick={e => { e.preventDefault(); setOpen(o => !o) }} className="w-4 text-[#5E6C84]">
|
||||
{open ? '▾' : '▸'}
|
||||
</button>
|
||||
)}
|
||||
<span>{name}</span>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user