frontend changes

This commit is contained in:
Chandini 2025-09-29 15:41:28 +05:30
parent 2176100b0e
commit b6d91b9a08
3 changed files with 209 additions and 27 deletions

View File

@ -0,0 +1,76 @@
"use client"
import { useEffect, useState } from "react"
import { getRepositoryStructure, type RepoStructureEntry } from "@/lib/api/github"
import { ChevronDown, ChevronRight, FileText, Folder } from "lucide-react"
type Node = {
name: string
path: string
type: 'file' | 'directory'
}
export default function RepoTree({ repositoryId, rootPath = "", onSelectFile, onSelectDirectory }: { repositoryId: string, rootPath?: string, onSelectFile: (path: string) => void, onSelectDirectory?: (path: string) => void }) {
const [expanded, setExpanded] = useState<Record<string, boolean>>({})
const [loading, setLoading] = useState<Record<string, boolean>>({})
const [children, setChildren] = useState<Record<string, Node[]>>({})
const toggle = async (nodePath: string) => {
const next = !expanded[nodePath]
setExpanded(prev => ({ ...prev, [nodePath]: next }))
if (next && !children[nodePath]) {
setLoading(prev => ({ ...prev, [nodePath]: true }))
try {
const res = await getRepositoryStructure(repositoryId, nodePath)
const list = (res?.structure || []) as RepoStructureEntry[]
const mapped: Node[] = list.map(e => ({
name: (e as any).name || (e as any).filename || (((e as any).path || (e as any).relative_path || '').split('/').slice(-1)[0]) || 'unknown',
path: (e as any).path || (e as any).relative_path || '',
type: (e as any).type === 'directory' || (e as any).type === 'dir' ? 'directory' : 'file'
}))
setChildren(prev => ({ ...prev, [nodePath]: mapped }))
onSelectDirectory && onSelectDirectory(nodePath)
} finally {
setLoading(prev => ({ ...prev, [nodePath]: false }))
}
}
}
// initial load for root
useEffect(() => {
toggle(rootPath)
onSelectDirectory && onSelectDirectory(rootPath)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [repositoryId, rootPath])
const renderNode = (node: Node) => {
const isDir = node.type === 'directory'
const isOpen = !!expanded[node.path]
return (
<div key={node.path} className="select-none">
<div className="flex items-center gap-1 py-1 px-1 hover:bg-white/5 rounded cursor-pointer"
onClick={() => isDir ? toggle(node.path) : onSelectFile(node.path)}>
{isDir ? (
isOpen ? <ChevronDown className="h-4 w-4 text-white/70"/> : <ChevronRight className="h-4 w-4 text-white/70"/>
) : (
<span className="w-4"/>
)}
{isDir ? <Folder className="h-4 w-4 mr-1"/> : <FileText className="h-4 w-4 mr-1"/>}
<span className="truncate text-sm text-white">{node.name}</span>
</div>
{isDir && isOpen && (
<div className="pl-5 border-l border-white/10 ml-2">
{loading[node.path] && <div className="text-xs text-white/60 py-1">Loading</div>}
{(children[node.path] || []).map(ch => renderNode(ch))}
</div>
)}
</div>
)
}
return (
<div className="text-white/90">
{(children[rootPath] || []).map(n => renderNode(n))}
</div>
)
}

View File

@ -7,6 +7,7 @@ import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { getRepositoryStructure, getRepositoryFileContent, getRepositoryCommitSummary, type RepoStructureEntry, type CommitSummaryResponse } from "@/lib/api/github"
import { ArrowLeft, BookText, Clock, Code, FileText, Folder, GitBranch, Search, GitCommit } from "lucide-react"
import RepoTree from "./RepoTree"
export default function RepoByIdClient({ repositoryId, initialPath = "" }: { repositoryId: string, initialPath?: string }) {
const [path, setPath] = useState(initialPath)
@ -15,6 +16,11 @@ export default function RepoByIdClient({ repositoryId, initialPath = "" }: { rep
const [fileQuery, setFileQuery] = useState("")
const [readme, setReadme] = useState<string | null>(null)
const [commitSummary, setCommitSummary] = useState<CommitSummaryResponse | null>(null)
const [selectedFile, setSelectedFile] = useState<string | null>(null)
const [previewLoading, setPreviewLoading] = useState(false)
const [previewContent, setPreviewContent] = useState<string | null>(null)
const [selectedDir, setSelectedDir] = useState<string>("")
const [dirEntries, setDirEntries] = useState<RepoStructureEntry[]>([])
const timeAgo = (iso?: string) => {
if (!iso) return ''
@ -73,9 +79,9 @@ export default function RepoByIdClient({ repositoryId, initialPath = "" }: { rep
return entries.filter(e => e.name.toLowerCase().includes(q))
}, [entries, fileQuery])
const navigateFolder = (name: string) => {
const next = path ? `${path}/${name}` : name
setPath(next)
const navigateFolder = (rel: string) => {
// rel is expected to be a relative path from repo root
setPath(rel)
}
const goUp = () => {
@ -130,31 +136,116 @@ export default function RepoByIdClient({ repositoryId, initialPath = "" }: { rep
</div>
)}
<Card className="bg-white/5 border-white/10 overflow-hidden">
<CardContent className="p-0">
{loading && (
<div className="px-4 py-6 text-white/60">Loading...</div>
{/* Three-column layout: left tree, middle directory listing, right preview */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
<div className="lg:col-span-1 bg-white/5 border border-white/10 rounded-lg p-2 overflow-auto max-h-[70vh]">
<RepoTree
repositoryId={repositoryId}
rootPath=""
onSelectDirectory={async (dirPath) => {
// Update center panel with this directory contents
setSelectedDir(dirPath)
try {
const res = await getRepositoryStructure(repositoryId, dirPath)
setDirEntries(res?.structure || [])
} catch {
setDirEntries([])
}
}}
onSelectFile={async (filePath) => {
setSelectedFile(filePath)
try {
setPreviewLoading(true)
const file = await getRepositoryFileContent(repositoryId, filePath)
setPreviewContent(file?.content || null)
} finally {
setPreviewLoading(false)
}
}}
/>
</div>
{/* Middle: Directory listing */}
<div className="lg:col-span-1 bg-white/5 border border-white/10 rounded-lg overflow-hidden">
<div className="border-b border-white/10 px-4 py-3 text-sm font-semibold flex items-center justify-between">
<span>Folder</span>
<span className="text-xs text-white/60 truncate ml-2">{selectedDir || "/"}</span>
</div>
<div className="divide-y divide-white/10">
{dirEntries.length === 0 && (
<div className="px-4 py-6 text-white/60">Select a folder from the tree.</div>
)}
{dirEntries.map((e, i) => {
const rawPath = (e as any).path || (e as any).relative_path || ''
const displayName = (e as any).name || (e as any).filename || (rawPath ? rawPath.split('/').slice(-1)[0] : '(unknown)')
const typeVal = (e as any).type || (e as any).entry_type
const isDirHint = typeVal === 'directory' || typeVal === 'dir' || (e as any).is_directory === true || (e as any).isDir === true
// heuristic fallback if type missing: no dot in last segment often means folder
const isDir = isDirHint || (!!displayName && !displayName.includes('.'))
return (
<div key={(e as any).path || (e as any).relative_path || displayName + i} className="flex items-center px-4 py-3 hover:bg-white/5 cursor-pointer"
onClick={async () => {
const relPath = (e as any).path || (e as any).relative_path || displayName
if (isDir) {
// drill down in tree and center
setSelectedDir(relPath)
try {
const res = await getRepositoryStructure(repositoryId, relPath)
setDirEntries(res?.structure || [])
} catch {
setDirEntries([])
}
return
}
// file
setSelectedFile(relPath)
try {
setPreviewLoading(true)
let file = await getRepositoryFileContent(repositoryId, relPath)
// if backend still reports directory, probe and navigate instead of showing error
if (!file?.content) {
try {
const probe = await getRepositoryStructure(repositoryId, relPath)
if (Array.isArray(probe?.structure) && probe.structure.length > 0) {
setPreviewContent(null)
setSelectedFile(null)
setSelectedDir(relPath)
setDirEntries(probe.structure)
return
}
} catch {}
}
setPreviewContent(file?.content || null)
} finally {
setPreviewLoading(false)
}
}}>
<div className="w-7 flex justify-center">
{isDir ? <Folder className="h-4 w-4"/> : <FileText className="h-4 w-4"/>}
</div>
<div className="flex-1 font-medium truncate text-white">{displayName}</div>
<div className="w-28 text-right text-sm text-white/60">{isDir ? 'folder' : 'file'}</div>
</div>
)
})}
</div>
</div>
{/* Right: Preview */}
<div className="lg:col-span-2 bg-white/5 border border-white/10 rounded-lg">
<div className="border-b border-white/10 px-4 py-3 text-sm font-semibold flex items-center justify-between">
<span>Preview</span>
{selectedFile && <span className="text-xs text-white/60 truncate ml-2">{selectedFile}</span>}
</div>
{previewLoading ? (
<div className="px-4 py-6 text-white/60">Loading file...</div>
) : !selectedFile ? (
<div className="p-6 text-white/60">Select a file to preview its contents.</div>
) : previewContent ? (
<pre className="p-4 whitespace-pre-wrap text-sm text-white/90 overflow-auto">{previewContent}</pre>
) : (
<div className="p-6 text-white/60">No preview available (binary or empty file).</div>
)}
{!loading && visible.length === 0 && (
<div className="px-4 py-6 text-white/60">No entries found.</div>
)}
{visible.map((e, i) => {
const displayName = (e as any).name || (e as any).filename || (e as any).relative_path || (e as any).path || '(unknown)'
const isDir = e.type === 'directory' || (e as any).type === 'dir'
return (
<div key={(e as any).path || (e as any).relative_path || displayName + i} className="flex items-center px-4 py-3 border-b border-white/10 hover:bg-white/5 cursor-pointer"
onClick={() => isDir ? navigateFolder(displayName) : void 0}>
<div className="w-7 flex justify-center">
{isDir ? <Folder className="h-4 w-4"/> : <FileText className="h-4 w-4"/>}
</div>
<div className="flex-1 font-medium truncate text-white">{displayName}</div>
<div className="w-40 text-right text-sm text-white/60 flex items-center justify-end gap-1">
<Clock className="h-4 w-4"/>
</div>
</div>
)})}
</CardContent>
</Card>
</div>
</div>
<Card className="bg-white/5 border-white/10">
<CardContent className="p-0">

View File

@ -108,6 +108,21 @@ export async function getRepositoryCommits(repositoryId: string, opts?: { page?:
return res.data?.data as CommitsListResponse
}
export interface ResolvePathInfo {
repository_id: string
local_path: string
requested_file_path: string
resolved_absolute_path: string | null
exists: boolean
is_directory: boolean
}
export async function resolveRepositoryPath(repositoryId: string, filePath: string): Promise<ResolvePathInfo> {
const url = `/api/github/repository/${encodeURIComponent(repositoryId)}/resolve-path?file_path=${encodeURIComponent(filePath)}`
const res = await authApiClient.get(url)
return res.data?.data as ResolvePathInfo
}
export interface AttachRepositoryResponse<T = unknown> {
success: boolean
message?: string